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 "config.h"
25 #include "httpsocket.h"
26 #include "httpserver.h"
27 #include "gui/settings.h"
28 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
29 #include "devices/cdparanoia.h"
30 #include "devices/extractjob.h"
31 #endif
32 #include <QTcpSocket>
33 #include <QStringList>
34 #include <QTextStream>
35 #include <QFile>
36 #include <QUrl>
37 #include <QNetworkProxy>
38 #include <QUrlQuery>
39 #include <QFileInfo>
40 #include <QDebug>
41 #define DBUG if (HttpServer::debugEnabled()) qWarning() << "HttpSocket" << __FUNCTION__
42 
43 static const quint64 constMaxBuffer = 32768;
44 
detectMimeType(const QString & file)45 static QString detectMimeType(const QString &file)
46 {
47     QString suffix = QFileInfo(file).suffix().toLower();
48     if (suffix == QLatin1String("mp3")) {
49         return QLatin1String("audio/mpeg");
50     }
51     if (suffix == QLatin1String("ogg")) {
52         return QLatin1String("audio/ogg");
53     }
54     if (suffix == QLatin1String("flac")) {
55         return QLatin1String("audio/x-flac");
56     }
57     if (suffix == QLatin1String("wma")) {
58         return QLatin1String("audio/x-ms-wma");
59     }
60     if (suffix == QLatin1String("m4a") || suffix == QLatin1String("m4b") || suffix == QLatin1String("m4p") || suffix == QLatin1String("mp4")) {
61         return QLatin1String("audio/mp4");
62     }
63     if (suffix == QLatin1String("wav")) {
64         return QLatin1String("audio/x-wav");
65     }
66     if (suffix == QLatin1String("wv") || suffix == QLatin1String("wvp")) {
67         return QLatin1String("audio/x-wavpack");
68     }
69     if (suffix == QLatin1String("ape")) {
70         return QLatin1String("audio/x-monkeys-audio"); // "audio/x-ape";
71     }
72     if (suffix == QLatin1String("spx")) {
73         return QLatin1String("audio/x-speex");
74     }
75     if (suffix == QLatin1String("tta")) {
76         return QLatin1String("audio/x-tta");
77     }
78     if (suffix == QLatin1String("aiff") || suffix == QLatin1String("aif") || suffix == QLatin1String("aifc")) {
79         return QLatin1String("audio/x-aiff");
80     }
81     if (suffix == QLatin1String("mpc") || suffix == QLatin1String("mpp") || suffix == QLatin1String("mp+")) {
82         return QLatin1String("audio/x-musepack");
83     }
84     if (suffix == QLatin1String("dff")) {
85         return QLatin1String("application/x-dff");
86     }
87     if (suffix == QLatin1String("dsf")) {
88         return QLatin1String("application/x-dsf");
89     }
90     if (suffix == QLatin1String("opus")) {
91         return QLatin1String("audio/opus");
92     }
93 
94     return QString();
95 }
96 
writeMimeType(const QString & mimeType,QTcpSocket * socket,qint32 from,qint32 size,bool allowSeek)97 static void writeMimeType(const QString &mimeType, QTcpSocket *socket, qint32 from, qint32 size, bool allowSeek)
98 {
99     if (!mimeType.isEmpty()) {
100         QTextStream os(socket);
101         os.setAutoDetectUnicode(true);
102         if (allowSeek) {
103             if (0==from) {
104                 os << "HTTP/1.0 200 OK"
105                    << "\r\nAccept-Ranges: bytes"
106                    << "\r\nContent-Length: " << QString::number(size)
107                    << "\r\nContent-Type: " << mimeType << "\r\n\r\n";
108             } else {
109                 os << "HTTP/1.0 200 OK"
110                    << "\r\nAccept-Ranges: bytes"
111                    << "\r\nContent-Range: bytes " << QString::number(from) << "-" << QString::number(size-1) << "/" << QString::number(size)
112                    << "\r\nContent-Length: " << QString::number(size-from)
113                    << "\r\nContent-Type: " << mimeType << "\r\n\r\n";
114             }
115             DBUG << mimeType << QString::number(size) << "Can seek";
116         } else {
117             os << "HTTP/1.0 200 OK"
118                << "\r\nContent-Length: " << QString::number(size)
119                << "\r\nContent-Type: " << mimeType << "\r\n\r\n";
120             DBUG << mimeType << QString::number(size);
121         }
122     }
123 }
124 
getSep(const QByteArray & a,int pos)125 static int getSep(const QByteArray &a, int pos)
126 {
127     for (int i=pos+1; i<a.length(); ++i) {
128         if ('\n'==a[i] || '\r'==a[i] || ' '==a[i]) {
129             return i;
130         }
131     }
132     return -1;
133 }
134 
split(const QByteArray & a)135 static QList<QByteArray> split(const QByteArray &a)
136 {
137     QList<QByteArray> rv;
138     int lastPos=-1;
139     for (;;) {
140         int pos=getSep(a, lastPos);
141 
142         if (pos==(lastPos+1)) {
143             lastPos++;
144         } else if (pos>-1) {
145             lastPos++;
146             rv.append(a.mid(lastPos, pos-lastPos));
147             lastPos=pos;
148         } else {
149             lastPos++;
150             rv.append(a.mid(lastPos));
151             break;
152         }
153     }
154     return rv;
155 }
156 
getRange(const QStringList & params,qint32 & from,qint32 & to)157 static void getRange(const QStringList &params, qint32 &from, qint32 &to)
158 {
159     for (const QString &str: params) {
160         if (str.startsWith("Range:")) {
161             int start=str.indexOf("bytes=");
162             if (start>0) {
163                 QStringList range=str.mid(start+6).split("-", QString::SkipEmptyParts);
164                 if (1==range.length()) {
165                     from=range.at(0).toLong();
166                 } else if (2==range.length()) {
167                     from=range.at(0).toLong();
168                     to=range.at(1).toLong();
169                 }
170             }
171             break;
172         }
173     }
174 }
175 
HttpSocket(const QString & iface,quint16 port)176 HttpSocket::HttpSocket(const QString &iface, quint16 port)
177     : QTcpServer(nullptr)
178     , cfgInterface(iface)
179     , terminated(false)
180 {
181     if (!openPort(port)) {
182         openPort(0);
183     }
184 
185     DBUG << isListening() << serverPort();
186 
187     connect(MPDConnection::self(), SIGNAL(socketAddress(QString)), this, SLOT(mpdAddress(QString)));
188     connect(MPDConnection::self(), SIGNAL(cantataStreams(QList<Song>,bool)), this, SLOT(cantataStreams(QList<Song>,bool)));
189     connect(MPDConnection::self(), SIGNAL(cantataStreams(QStringList)), this, SLOT(cantataStreams(QStringList)));
190     connect(MPDConnection::self(), SIGNAL(removedIds(QSet<qint32>)), this, SLOT(removedIds(QSet<qint32>)));
191     connect(this, SIGNAL(newConnection()), SLOT(handleNewConnection()));
192 }
193 
openPort(quint16 p)194 bool HttpSocket::openPort(quint16 p)
195 {
196     setProxy(QNetworkProxy::NoProxy);
197     if (listen(QHostAddress::Any, p)) {
198         return true;
199     }
200     if (listen(QHostAddress::LocalHost, p)) {
201         return true;
202     }
203 
204     return false;
205 }
206 
terminate()207 void HttpSocket::terminate()
208 {
209     if (terminated) {
210         return;
211     }
212     DBUG;
213     terminated=true;
214     close();
215     deleteLater();
216 }
217 
handleNewConnection()218 void HttpSocket::handleNewConnection()
219 {
220     DBUG;
221     while (hasPendingConnections()) {
222         QTcpSocket *socket = nextPendingConnection();
223 
224         // prevent clients from sending too much data
225         socket->setReadBufferSize(constMaxBuffer);
226 
227         static const QLatin1String constIpV6Prefix("::ffff:");
228 
229         QString peer=socket->peerAddress().toString();
230         QString ifaceAddress=serverAddress().toString();
231         const bool hostOk=peer==ifaceAddress || peer==mpdAddr || peer==(constIpV6Prefix+mpdAddr) ||
232                           peer==QLatin1String("127.0.0.1") || peer==(constIpV6Prefix+QLatin1String("127.0.0.1"));
233 
234         DBUG << "peer:" << peer << "mpd:" << mpdAddr << "iface:" << ifaceAddress << "ok:" << hostOk;
235         if (!hostOk) {
236             sendErrorResponse(socket, 400);
237             socket->close();
238             DBUG << "Not from valid host";
239             return;
240         }
241 
242         connect(socket, SIGNAL(readyRead()), this, SLOT(readClient()));
243         connect(socket, SIGNAL(disconnected()), this, SLOT(discardClient()));
244     }
245 }
246 
readClient()247 void HttpSocket::readClient()
248 {
249     if (terminated) {
250         return;
251     }
252 
253     QTcpSocket *socket = static_cast<QTcpSocket *>(sender());
254     if (!socket) {
255         return;
256     }
257 
258     if (socket->bytesAvailable() >= constMaxBuffer) {
259         // Request too large, reject
260         sendErrorResponse(socket, 400);
261         socket->close();
262         DBUG << "Request too large";
263         return;
264     }
265 
266     if (socket->canReadLine()) {
267         QList<QByteArray> tokens = split(socket->readLine()); // QRegExp("[ \r\n][ \r\n]*"));
268         if (tokens.length()>=2 && "GET"==tokens[0]) {
269             QStringList params = QString(socket->readAll()).split(QRegExp("[\r\n][\r\n]*"));
270 
271             DBUG << "params" << params << "tokens" << tokens;
272             QUrl url(QUrl::fromEncoded(tokens[1]));
273             QUrlQuery q(url);
274             bool ok=false;
275             qint32 readBytesFrom=0;
276             qint32 readBytesTo=0;
277             getRange(params, readBytesFrom, readBytesTo);
278 
279             DBUG << "readBytesFrom" << readBytesFrom << "readBytesTo" << readBytesTo;
280             if (q.hasQueryItem("cantata")) {
281                 Song song=HttpServer::self()->decodeUrl(url);
282 
283                 if (!isCantataStream(song.file)) {
284                     sendErrorResponse(socket, 400);
285                     socket->close();
286                     DBUG << "Not cantata stream file";
287                     return;
288                 }
289 
290                 if (song.isCdda()) {
291                     #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
292                     QStringList parts=song.file.split("/", QString::SkipEmptyParts);
293                     if (parts.length()>=3) {
294                         QString dev=QLatin1Char('/')+parts.at(1)+QLatin1Char('/')+parts.at(2);
295                         CdParanoia cdparanoia(dev, false, false, true, Settings::self()->paranoiaOffset());
296 
297                         if (cdparanoia) {
298                             int firstSector = cdparanoia.firstSectorOfTrack(song.id);
299                             int lastSector = cdparanoia.lastSectorOfTrack(song.id);
300                             qint32 totalSize = ((lastSector-firstSector)+1)*CD_FRAMESIZE_RAW;
301                             int count = 0;
302                             bool writeHeader=0==readBytesFrom; // Only write header if we are not seeking...
303 //                            int bytesToDiscard = 0; // Number of bytes to discard in first read sector due to range request in HTTP header
304 //                            if (readBytesFrom>=ExtractJob::constWavHeaderSize) {
305 //                                readBytesFrom-=ExtractJob::constWavHeaderSize;
306 //                            }
307 
308 //                            if (readBytesFrom>0) {
309 //                                int sectorsToSeek=readBytesFrom/CD_FRAMESIZE_RAW;
310 //                                firstSector+=sectorsToSeek;
311 //                                bytesToDiscard=readBytesFrom-(sectorsToSeek*CD_FRAMESIZE_RAW);
312 //                            }
313                             cdparanoia.seek(firstSector, SEEK_SET);
314                             ok=true;
315                             writeMimeType(QLatin1String("audio/x-wav"), socket, readBytesFrom, totalSize+ExtractJob::constWavHeaderSize, false);
316                             if (writeHeader) {
317                                 ExtractJob::writeWavHeader(*socket, totalSize);
318                             }
319                             bool stop=false;
320                             while (!terminated && (firstSector+count) <= lastSector && !stop) {
321                                 qint16 *buf = cdparanoia.read();
322                                 if (!buf) {
323                                     break;
324                                 }
325                                 char *buffer=(char *)buf;
326                                 qint32 writePos=0;
327                                 qint32 toWrite=CD_FRAMESIZE_RAW;
328 
329 //                                if (bytesToDiscard>0) {
330 //                                    int toSkip=qMin(toWrite, bytesToDiscard);
331 //                                    writePos=toSkip;
332 //                                    toWrite-=toSkip;
333 //                                    bytesToDiscard-=toSkip;
334 //                                }
335 
336                                 if (toWrite>0 && !write(socket, &buffer[writePos], toWrite, stop)) {
337                                     break;
338                                 }
339                                 count++;
340                             }
341                         }
342                     }
343                     #endif
344                 } else if (!song.file.isEmpty()) {
345                     #ifdef Q_OS_WIN
346                     if (tokens[1].startsWith("//") && !song.file.startsWith(QLatin1String("//")) && !QFile::exists(song.file)) {
347                         QString share=QLatin1String("//")+url.host()+song.file;
348                         if (QFile::exists(share)) {
349                             song.file=share;
350                             DBUG << "fixed share-path" << song.file;
351                         }
352                     }
353                     #endif
354 
355                     QFile f(song.file);
356 
357                     if (f.open(QIODevice::ReadOnly)) {
358                         qint32 totalBytes = f.size();
359 
360                         writeMimeType(detectMimeType(song.file), socket, readBytesFrom, totalBytes, true);
361                         ok=true;
362                         qint32 readPos = 0;
363                         qint32 bytesRead = 0;
364 
365                         if (0!=readBytesFrom) {
366                             if (!f.seek(readBytesFrom)) {
367                                 ok=false;
368                             }
369                             bytesRead+=readBytesFrom;
370                         }
371 
372                         if (0!=readBytesTo && readBytesTo>readBytesFrom && readBytesTo!=totalBytes) {
373                             totalBytes-=(totalBytes-readBytesTo);
374                         }
375 
376                         if (ok) {
377                             static const int constChunkSize=32768;
378                             char buffer[constChunkSize];
379                             bool stop=false;
380                             do {
381                                 bytesRead = f.read(buffer, constChunkSize);
382                                 readPos+=bytesRead;
383                                 if (!write(socket, buffer, bytesRead, stop) || f.atEnd()) {
384                                     break;
385                                 }
386                             } while (readPos<totalBytes && !stop && !terminated);
387                         }
388                     } else {
389                         DBUG << "Failed to open" << song.file;
390                     }
391                 }
392             }
393 
394             if (!ok) {
395                 sendErrorResponse(socket, 404);
396             }
397 
398             socket->close();
399 
400             if (QTcpSocket::UnconnectedState==socket->state()) {
401                 socket->deleteLater();
402             }
403         } else {
404             // Bad Request
405             sendErrorResponse(socket, 400);
406             socket->close();
407             DBUG << "Bad Request";
408             return;
409         }
410     }
411 }
412 
discardClient()413 void HttpSocket::discardClient()
414 {
415     static_cast<QTcpSocket *>(sender())->deleteLater();
416 }
417 
mpdAddress(const QString & a)418 void HttpSocket::mpdAddress(const QString &a)
419 {
420     mpdAddr=a;
421 }
422 
isCantataStream(const QString & file) const423 bool HttpSocket::isCantataStream(const QString &file) const
424 {
425     DBUG << file << newlyAddedFiles.contains(file) << streamIds.values().contains(file);
426     return newlyAddedFiles.contains(file) || streamIds.values().contains(file);
427 }
428 
sendErrorResponse(QTcpSocket * socket,int code)429 void HttpSocket::sendErrorResponse(QTcpSocket *socket, int code)
430 {
431     QTextStream os(socket);
432     os.setAutoDetectUnicode(true);
433     os << "HTTP/1.0 " << code << " OK\r\n"
434           "Content-Type: text/html; charset=\"utf-8\"\r\n"
435           "\r\n";
436 }
437 
cantataStreams(const QStringList & files)438 void HttpSocket::cantataStreams(const QStringList &files)
439 {
440     DBUG << files;
441     for (const QString &f: files) {
442         Song s=HttpServer::self()->decodeUrl(f);
443         if (s.isCantataStream() || s.isCdda()) {
444             DBUG << s.file;
445             newlyAddedFiles+=s.file;
446         }
447     }
448 }
449 
cantataStreams(const QList<Song> & songs,bool isUpdate)450 void HttpSocket::cantataStreams(const QList<Song> &songs, bool isUpdate)
451 {
452     DBUG << isUpdate << songs.count();
453     if (!isUpdate) {
454         streamIds.clear();
455     }
456 
457     for (const Song &s: songs) {
458         DBUG << s.file;
459         if (s.isCantataStream()) {
460             streamIds.insert(s.id, HttpServer::self()->decodeUrl(s.file).file);
461         } else {
462             streamIds.insert(s.id, s.file);
463         }
464         newlyAddedFiles.remove(s.file);
465     }
466     DBUG << streamIds;
467 }
468 
removedIds(const QSet<qint32> & ids)469 void HttpSocket::removedIds(const QSet<qint32> &ids)
470 {
471     for (qint32 id: ids) {
472         streamIds.remove(id);
473     }
474 }
475 
write(QTcpSocket * socket,char * buffer,qint32 bytesRead,bool & stop)476 bool HttpSocket::write(QTcpSocket *socket, char *buffer, qint32 bytesRead, bool &stop)
477 {
478     if (bytesRead<0 || terminated) {
479         return false;
480     }
481 
482     qint32 writePos=0;
483     do {
484         qint32 bytesWritten = socket->write(&buffer[writePos], bytesRead - writePos);
485         if (terminated || -1==bytesWritten) {
486             stop=true;
487             break;
488         }
489         socket->flush();
490         writePos+=bytesWritten;
491     } while (writePos<bytesRead);
492 
493     if (QAbstractSocket::ConnectedState==socket->state()) {
494         socket->waitForBytesWritten();
495     }
496     if (QAbstractSocket::ConnectedState!=socket->state()) {
497         return false;
498     }
499     return true;
500 }
501 
502 #include "moc_httpsocket.cpp"
503