File indexing completed on 2024-05-19 04:55:59

0001 /**
0002  * \file httpclient.cpp
0003  * Client to connect to HTTP server.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 30 Dec 2008
0008  *
0009  * Copyright (C) 2008-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "httpclient.h"
0028 #include <QNetworkAccessManager>
0029 #include <QNetworkRequest>
0030 #include <QNetworkProxy>
0031 #include <QByteArray>
0032 #include <QTimer>
0033 #include <QDateTime>
0034 #include "networkconfig.h"
0035 
0036 
0037 /** Time when last request was sent to server */
0038 QMap<QString, QDateTime> HttpClient::s_lastRequestTime;
0039 /** Minimum interval between two requests to server in ms */
0040 QMap<QString, int> HttpClient::s_minimumRequestInterval;
0041 
0042 /**
0043  * Used to initialize the minimum interval for some servers
0044  * at static initialization time.
0045  *
0046  * Rate limit requests to servers, MusicBrainz and Discogs impose a limit of
0047  * one request per second
0048  * http://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#Source_IP_address
0049  * http://www.discogs.com/developers/accessing.html#rate-limiting
0050  */
0051 static struct MinimumRequestIntervalInitializer {
0052   MinimumRequestIntervalInitializer() {
0053     HttpClient::s_minimumRequestInterval[QLatin1String("musicbrainz.org")] = 1000;
0054     HttpClient::s_minimumRequestInterval[QLatin1String("api.discogs.com")] = 1000;
0055     HttpClient::s_minimumRequestInterval[QLatin1String("www.discogs.com")] = 1000;
0056     HttpClient::s_minimumRequestInterval[QLatin1String("www.amazon.com")] = 1000;
0057     HttpClient::s_minimumRequestInterval[QLatin1String("images.amazon.com")] = 1000;
0058     HttpClient::s_minimumRequestInterval[QLatin1String("www.gnudb.org")] = 1000;
0059     HttpClient::s_minimumRequestInterval[QLatin1String("gnudb.gnudb.org")] = 1000;
0060     HttpClient::s_minimumRequestInterval[QLatin1String("api.acoustid.org")] = 1000;
0061   }
0062 } minimumRequestIntervalInitializer;
0063 
0064 /**
0065  * Constructor.
0066  *
0067  * @param netMgr  network access manager
0068  */
0069 HttpClient::HttpClient(QNetworkAccessManager* netMgr)
0070   : QObject(netMgr), m_netMgr(netMgr), m_rcvBodyLen(0),
0071     m_requestTimer(new QTimer(this))
0072 {
0073   setObjectName(QLatin1String("HttpClient"));
0074   m_requestTimer->setSingleShot(true);
0075   connect(m_requestTimer, &QTimer::timeout, this, &HttpClient::delayedSendRequest);
0076 }
0077 
0078 /**
0079  * Destructor.
0080  */
0081 HttpClient::~HttpClient()
0082 {
0083   if (m_reply) {
0084     m_reply->close();
0085     m_reply->disconnect();
0086     m_reply->deleteLater();
0087   }
0088 }
0089 
0090 /** Only defined for generation of translation files */
0091 #define DATA_RECEIVED_FOR_PO QT_TRANSLATE_NOOP("@default", "Data received: %1")
0092 
0093 /**
0094  * Called when the request is finished.
0095  */
0096 void HttpClient::networkReplyFinished()
0097 {
0098   if (auto reply = qobject_cast<QNetworkReply*>(sender())) {
0099     QByteArray data(reply->readAll());
0100     m_rcvBodyType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
0101     m_rcvBodyLen = reply->header(QNetworkRequest::ContentLengthHeader).toUInt();
0102     QString msg(tr("Ready."));
0103     if (reply->error() != QNetworkReply::NoError) {
0104       msg = tr("Error");
0105       msg += QLatin1String(": ");
0106       msg += reply->errorString();
0107     } else {
0108       if (QVariant redirect =
0109             reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
0110           !redirect.isNull()) {
0111         QUrl redirectUrl = redirect.toUrl();
0112         if (redirectUrl.isRelative()) {
0113           redirectUrl = reply->url().resolved(redirectUrl);
0114         }
0115         if (redirectUrl.isValid()) {
0116           reply->deleteLater();
0117 
0118           QNetworkRequest request(redirectUrl);
0119           reply = m_netMgr->get(request);
0120           m_reply = reply;
0121           connect(reply, &QNetworkReply::finished,
0122                   this, &HttpClient::networkReplyFinished);
0123           connect(reply, &QNetworkReply::downloadProgress,
0124                   this, &HttpClient::networkReplyProgress);
0125 #if QT_VERSION >= 0x050f00
0126           connect(reply, &QNetworkReply::errorOccurred,
0127                   this, &HttpClient::networkReplyError);
0128 #else
0129           connect(reply,
0130                   static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(
0131                     &QNetworkReply::error),
0132                   this, &HttpClient::networkReplyError);
0133 #endif
0134           return;
0135         }
0136       }
0137     }
0138     emit bytesReceived(data);
0139     emitProgress(msg, data.size(), data.size());
0140     reply->deleteLater();
0141   }
0142 }
0143 
0144 /**
0145  * Called to report connection progress.
0146  *
0147  * @param received bytes received
0148  * @param total total bytes
0149  */
0150 void HttpClient::networkReplyProgress(qint64 received, qint64 total)
0151 {
0152   emitProgress(tr("Data received: %1").arg(received), received, total);
0153 }
0154 
0155 /**
0156  * Called when an error occurred.
0157  */
0158 void HttpClient::networkReplyError(QNetworkReply::NetworkError)
0159 {
0160   if (auto reply = qobject_cast<QNetworkReply*>(sender())) {
0161     emitProgress(reply->errorString(), -1, -1);
0162   }
0163 }
0164 
0165 /**
0166  * Send a HTTP GET request.
0167  *
0168  * @param url URL
0169  * @param headers optional raw headers to send
0170  */
0171 void HttpClient::sendRequest(const QUrl& url, const RawHeaderMap& headers)
0172 {
0173   QString host = url.host();
0174   qint64 msSinceLastRequest;
0175   int minimumRequestInterval;
0176   QDateTime now = QDateTime::currentDateTime();
0177   QDateTime lastRequestTime = s_lastRequestTime.value(host);
0178   if (lastRequestTime.isValid() &&
0179       (minimumRequestInterval = s_minimumRequestInterval.value(host)) > 0 &&
0180       (msSinceLastRequest = lastRequestTime.msecsTo(now)) <
0181       minimumRequestInterval) {
0182     // Delay request to comply with minimum interval
0183     m_delayedSendRequestContext.url = url;
0184     m_delayedSendRequestContext.headers = headers;
0185     m_requestTimer->start(minimumRequestInterval -
0186                           static_cast<int>(msSinceLastRequest));
0187     return;
0188   }
0189 
0190   m_rcvBodyLen = 0;
0191   m_rcvBodyType = QLatin1String("");
0192   QString proxy, username, password;
0193   int proxyPort = 0;
0194   QNetworkProxy::ProxyType proxyType = QNetworkProxy::NoProxy;
0195   const NetworkConfig& networkCfg = NetworkConfig::instance();
0196   if (networkCfg.useProxy()) {
0197     splitNamePort(networkCfg.proxy(), proxy, proxyPort);
0198     proxyType = QNetworkProxy::HttpProxy;
0199   }
0200   if (networkCfg.useProxyAuthentication()) {
0201     username = networkCfg.proxyUserName();
0202     password = networkCfg.proxyPassword();
0203   }
0204   m_netMgr->setProxy(QNetworkProxy(proxyType, proxy,
0205                                    static_cast<quint16>(proxyPort),
0206                                    username, password));
0207 
0208   QNetworkRequest request(url);
0209   for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) {
0210     request.setRawHeader(it.key(), it.value());
0211   }
0212   QNetworkReply* reply = m_netMgr->get(request);
0213   m_reply = reply;
0214   connect(reply, &QNetworkReply::finished,
0215           this, &HttpClient::networkReplyFinished);
0216   connect(reply, &QNetworkReply::downloadProgress,
0217           this, &HttpClient::networkReplyProgress);
0218 #if QT_VERSION >= 0x050f00
0219   connect(reply, &QNetworkReply::errorOccurred,
0220           this, &HttpClient::networkReplyError);
0221 #else
0222   connect(reply,
0223           static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(
0224             &QNetworkReply::error),
0225           this, &HttpClient::networkReplyError);
0226 #endif
0227   s_lastRequestTime[host] = now;
0228   emitProgress(tr("Request sent..."), 0, 0);
0229 }
0230 
0231 /**
0232  * Send a HTTP GET request.
0233  *
0234  * @param server host name
0235  * @param path   path of the URL
0236  * @param scheme scheme, default is "http"
0237  * @param headers optional raw headers to send
0238  */
0239 void HttpClient::sendRequest(const QString& server, const QString& path,
0240                              const QString& scheme, const RawHeaderMap& headers)
0241 {
0242   QString host(server);
0243   if (host.endsWith(QLatin1String(":80"))) {
0244     host.chop(3);
0245   }
0246   QUrl url;
0247   url.setUrl(scheme + QLatin1String("://") + host + path);
0248   sendRequest(url, headers);
0249 }
0250 
0251 /**
0252  * Called to start delayed sendRequest().
0253  */
0254 void HttpClient::delayedSendRequest()
0255 {
0256   sendRequest(m_delayedSendRequestContext.url,
0257               m_delayedSendRequestContext.headers);
0258 }
0259 
0260 /**
0261  * Abort request.
0262  */
0263 void HttpClient::abort()
0264 {
0265   if (m_reply) {
0266     m_reply->abort();
0267   }
0268 }
0269 
0270 /**
0271  * Emit a progress signal with step/total steps.
0272  *
0273  * @param text       state text
0274  * @param step       current step
0275  * @param totalSteps total number of steps
0276  */
0277 void HttpClient::emitProgress(const QString& text, int step, int totalSteps)
0278 {
0279   emit progress(text, step, totalSteps);
0280 }
0281 
0282 /**
0283  * Extract name and port from string.
0284  *
0285  * @param namePort input string with "name:port"
0286  * @param name     output string with "name"
0287  * @param port     output integer with port
0288  */
0289 void HttpClient::splitNamePort(const QString& namePort,
0290                                  QString& name, int& port)
0291 {
0292   if (int colPos = namePort.lastIndexOf(QLatin1Char(':')); colPos >= 0) {
0293     bool ok;
0294 #if QT_VERSION >= 0x060000
0295     port = namePort.mid(colPos + 1).toInt(&ok);
0296 #else
0297     port = namePort.midRef(colPos + 1).toInt(&ok);
0298 #endif
0299     if (!ok) port = 80;
0300     name = namePort.left(colPos);
0301   } else {
0302     name = namePort;
0303     port = 80;
0304   }
0305 }