/*
 * Bittorrent Client using Qt4 and libtorrent.
 * Copyright (C) 2006  Ishan Arora and Christophe Dumez
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * In addition, as a special exception, the copyright holders give permission to
 * link this program with the OpenSSL project's "OpenSSL" library (or with
 * modified versions of it that use the same license as the "OpenSSL" library),
 * and distribute the linked executables. You must obey the GNU General Public
 * License in all respects for all of the code used other than "OpenSSL".  If you
 * modify file(s), you may extend this exception to your version of the file(s),
 * but you are not obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 *
 * Contact : chris@qbittorrent.org
 */


#include "httpconnection.h"
#include "httpserver.h"
#include "httprequestheader.h"
#include "httpresponseheader.h"
#include "preferences.h"
#include "btjson.h"
#include "prefjson.h"
#include "qbtsession.h"
#include "misc.h"
#include "fs_utils.h"
#ifndef DISABLE_GUI
#include "iconprovider.h"
#endif
#include <QTcpSocket>
#include <QDateTime>
#include <QStringList>
#include <QFile>
#include <QDebug>
#include <QRegExp>
#include <QTemporaryFile>
#include <queue>
#include <vector>

using namespace libtorrent;

HttpConnection::HttpConnection(QTcpSocket *socket, HttpServer *parent)
  : QObject(parent), m_socket(socket), m_httpserver(parent)
{
  m_socket->setParent(this);
  connect(m_socket, SIGNAL(readyRead()), SLOT(read()));
  connect(m_socket, SIGNAL(disconnected()), SLOT(deleteLater()));
}

HttpConnection::~HttpConnection() {
  delete m_socket;
}

void HttpConnection::processDownloadedFile(const QString &url,
                                           const QString &file_path) {
  qDebug("URL %s successfully downloaded !", qPrintable(url));
  emit torrentReadyToBeDownloaded(file_path, false, url, false);
}

void HttpConnection::handleDownloadFailure(const QString& url,
                                           const QString& reason) {
  std::cerr << "Could not download " << qPrintable(url) << ", reason: "
            << qPrintable(reason) << std::endl;
}

void HttpConnection::read()
{
  m_receivedData.append(m_socket->readAll());

  // Parse HTTP request header
  const int header_end = m_receivedData.indexOf("\r\n\r\n");
  if (header_end < 0) {
    qDebug() << "Partial request: \n" << m_receivedData;
    // Partial request waiting for the rest
    return;
  }

  const QByteArray header = m_receivedData.left(header_end);
  m_parser.writeHeader(header);
  if (m_parser.isError()) {
    qWarning() << Q_FUNC_INFO << "header parsing error";
    m_receivedData.clear();
    m_generator.setStatusLine(400, "Bad Request");
    m_generator.setContentEncoding(m_parser.acceptsEncoding());
    write();
    return;
  }

  // Parse HTTP request message
  if (m_parser.header().hasContentLength())  {
    const int expected_length = m_parser.header().contentLength();
    QByteArray message = m_receivedData.mid(header_end + 4, expected_length);

    if (expected_length > 10000000 /* ~10MB */) {
      qWarning() << "Bad request: message too long";
      m_generator.setStatusLine(400, "Bad Request");
      m_generator.setContentEncoding(m_parser.acceptsEncoding());
      m_receivedData.clear();
      write();
      return;
    }

    if (message.length() < expected_length) {
      // Message too short, waiting for the rest
      qDebug() << "Partial message:\n" << message;
      return;
    }

    m_parser.writeMessage(message);
    m_receivedData = m_receivedData.mid(header_end + 4 + expected_length);
  } else {
    m_receivedData.clear();
  }

  if (m_parser.isError()) {
    qWarning() << Q_FUNC_INFO << "message parsing error";
    m_generator.setStatusLine(400, "Bad Request");
    m_generator.setContentEncoding(m_parser.acceptsEncoding());
    write();
  } else {
    respond();
  }
}

void HttpConnection::write()
{
  m_socket->write(m_generator.toByteArray());
  m_socket->disconnectFromHost();
}

void HttpConnection::translateDocument(QString& data) {
  static QRegExp regex(QString::fromUtf8("_\\(([\\w\\s?!:\\/\\(\\),%µ&\\-\\.]+)\\)"));
  static QRegExp mnemonic("\\(?&([a-zA-Z]?\\))?");
  const std::string contexts[] = {"TransferListFiltersWidget", "TransferListWidget",
                                  "PropertiesWidget", "MainWindow", "HttpServer",
                                  "confirmDeletionDlg", "TrackerList", "TorrentFilesModel",
                                  "options_imp", "Preferences", "TrackersAdditionDlg",
                                  "ScanFoldersModel", "PropTabBar", "TorrentModel",
                                  "downloadFromURL", "misc"};
  const size_t context_count = sizeof(contexts)/sizeof(contexts[0]);
  int i = 0;
  bool found = true;

  const QString locale = Preferences::instance()->getLocale();
  bool isTranslationNeeded = !locale.startsWith("en") || locale.startsWith("en_AU") || locale.startsWith("en_GB");

  while(i < data.size() && found) {
    i = regex.indexIn(data, i);
    if (i >= 0) {
      //qDebug("Found translatable string: %s", regex.cap(1).toUtf8().data());
      QByteArray word = regex.cap(1).toUtf8();

      QString translation = word;
      if (isTranslationNeeded) {
        size_t context_index = 0;
        while(context_index < context_count && translation == word) {
#if (QT_VERSION < QT_VERSION_CHECK(5, 0, 0))
          translation = qApp->translate(contexts[context_index].c_str(), word.constData(), 0, QCoreApplication::UnicodeUTF8, 1);
#else
          translation = qApp->translate(contexts[context_index].c_str(), word.constData(), 0, 1);
#endif
          ++context_index;
        }
      }
      // Remove keyboard shortcuts
      translation.replace(mnemonic, "");

      data.replace(i, regex.matchedLength(), translation);
      i += translation.length();
    } else {
        found = false; // no more translatable strings
    }
  }
}

void HttpConnection::respond() {
  if ((m_socket->peerAddress() != QHostAddress::LocalHost
      && m_socket->peerAddress() != QHostAddress::LocalHostIPv6)
     || m_httpserver->isLocalAuthEnabled()) {
    // Authentication
    const QString peer_ip = m_socket->peerAddress().toString();
    const int nb_fail = m_httpserver->NbFailedAttemptsForIp(peer_ip);
    if (nb_fail >= MAX_AUTH_FAILED_ATTEMPTS) {
      m_generator.setStatusLine(403, "Forbidden");
      m_generator.setMessage(tr("Your IP address has been banned after too many failed authentication attempts."));
      m_generator.setContentEncoding(m_parser.acceptsEncoding());
      write();
      return;
    }
    QString auth = m_parser.header().value("Authorization");
    if (auth.isEmpty()) {
      // Return unauthorized header
      qDebug("Auth is Empty...");
      m_generator.setStatusLine(401, "Unauthorized");
      m_generator.setValue("WWW-Authenticate",  "Digest realm=\""+QString(QBT_REALM)+"\", nonce=\""+m_httpserver->generateNonce()+"\", opaque=\""+m_httpserver->generateNonce()+"\", stale=\"false\", algorithm=\"MD5\", qop=\"auth\"");
      m_generator.setContentEncoding(m_parser.acceptsEncoding());
      write();
      return;
    }
    //qDebug("Auth: %s", qPrintable(auth.split(" ").first()));
    if (QString::compare(auth.split(" ").first(), "Digest", Qt::CaseInsensitive) != 0
        || !m_httpserver->isAuthorized(auth.toUtf8(), m_parser.header().method())) {
      // Update failed attempt counter
      m_httpserver->increaseNbFailedAttemptsForIp(peer_ip);
      qDebug("client IP: %s (%d failed attempts)", qPrintable(peer_ip), nb_fail);
      // Return unauthorized header
      m_generator.setStatusLine(401, "Unauthorized");
      m_generator.setValue("WWW-Authenticate",  "Digest realm=\""+QString(QBT_REALM)+"\", nonce=\""+m_httpserver->generateNonce()+"\", opaque=\""+m_httpserver->generateNonce()+"\", stale=\"false\", algorithm=\"MD5\", qop=\"auth\"");
      m_generator.setContentEncoding(m_parser.acceptsEncoding());
      write();
      return;
    }
    // Client successfully authenticated, reset number of failed attempts
    m_httpserver->resetNbFailedAttemptsForIp(peer_ip);
  }
  QString url  = m_parser.url();
  // Favicon
  if (url.endsWith("favicon.ico")) {
    qDebug("Returning favicon");
    QFile favicon(":/Icons/skin/qbittorrent16.png");
    if (favicon.open(QIODevice::ReadOnly)) {
      const QByteArray data = favicon.readAll();
      favicon.close();
      m_generator.setStatusLine(200, "OK");
      m_generator.setContentTypeByExt("png");
      m_generator.setMessage(data);
      m_generator.setContentEncoding(m_parser.acceptsEncoding());
      write();
    } else {
      respondNotFound();
    }
    return;
  }

  QStringList list = url.split('/', QString::SkipEmptyParts);
  if (list.contains(".") || list.contains("..")) {
    respondNotFound();
    return;
  }

  if (list.isEmpty())
    list.append("index.html");

  if (list.size() >= 2) {
    if (list[0] == "json") {
      if (list[1] == "torrents") {
        respondTorrentsJson();
        return;
      }
      if (list.size() > 2) {
        if (list[1] == "propertiesGeneral") {
          const QString& hash = list[2];
          respondGenPropertiesJson(hash);
          return;
        }
        if (list[1] == "propertiesTrackers") {
          const QString& hash = list[2];
          respondTrackersPropertiesJson(hash);
          return;
        }
        if (list[1] == "propertiesFiles") {
          const QString& hash = list[2];
          respondFilesPropertiesJson(hash);
          return;
        }
      } else {
        if (list[1] == "preferences") {
          respondPreferencesJson();
          return;
        } else {
          if (list[1] == "transferInfo") {
            respondGlobalTransferInfoJson();
            return;
          }
        }
      }
    }
    if (list[0] == "command") {
      const QString& command = list[1];
      if (command == "shutdown") {
        qDebug() << "Shutdown request from Web UI";
        // Special case handling for shutdown, we
        // need to reply to the Web UI before
        // actually shutting down.
        m_generator.setStatusLine(200, "OK");
        m_generator.setContentEncoding(m_parser.acceptsEncoding());
        write();
        qApp->processEvents();
        // Exit application
        qApp->exit();
      } else {
        respondCommand(command);
        m_generator.setStatusLine(200, "OK");
        m_generator.setContentEncoding(m_parser.acceptsEncoding());
        write();
      }
      return;
    }
  }

  // Icons from theme
  //qDebug() << "list[0]" << list[0];
  if (list[0] == "theme" && list.size() == 2) {
#ifdef DISABLE_GUI
    url = ":/Icons/oxygen/"+list[1]+".png";
#else
    url = IconProvider::instance()->getIconPath(list[1]);
#endif
    qDebug() << "There icon:" << url;
  } else {
    if (list[0] == "images") {
      list[0] = "Icons";
    } else {
      if (list.last().endsWith(".html"))
        list.prepend("html");
      list.prepend("webui");
    }
    url = ":/" + list.join("/");
  }
  QFile file(url);
  if (!file.open(QIODevice::ReadOnly)) {
    qDebug("File %s was not found!", qPrintable(url));
    respondNotFound();
    return;
  }
  QString ext = list.last();
  int index = ext.lastIndexOf('.') + 1;
  if (index > 0)
    ext.remove(0, index);
  else
    ext.clear();
  QByteArray data = file.readAll();
  file.close();

  // Translate the page
  if (ext == "html" || (ext == "js" && !list.last().startsWith("excanvas"))) {
    QString dataStr = QString::fromUtf8(data.constData());
    translateDocument(dataStr);
    if (url.endsWith("about.html")) {
      dataStr.replace("${VERSION}", VERSION);
    }
    data = dataStr.toUtf8();
  }
  m_generator.setStatusLine(200, "OK");
  m_generator.setContentTypeByExt(ext);
  m_generator.setMessage(data);
  m_generator.setContentEncoding(m_parser.acceptsEncoding());
  write();
}

void HttpConnection::respondNotFound() {
  m_generator.setStatusLine(404, "File not found");
  m_generator.setContentEncoding(m_parser.acceptsEncoding());
  write();
}

void HttpConnection::respondTorrentsJson() {
  m_generator.setStatusLine(200, "OK");
  m_generator.setContentTypeByExt("js");
  m_generator.setMessage(btjson::getTorrents());
  m_generator.setContentEncoding(m_parser.acceptsEncoding());
  write();
}

void HttpConnection::respondGenPropertiesJson(const QString& hash) {
  m_generator.setStatusLine(200, "OK");
  m_generator.setContentTypeByExt("js");
  m_generator.setMessage(btjson::getPropertiesForTorrent(hash));
  m_generator.setContentEncoding(m_parser.acceptsEncoding());
  write();
}

void HttpConnection::respondTrackersPropertiesJson(const QString& hash) {
  m_generator.setStatusLine(200, "OK");
  m_generator.setContentTypeByExt("js");
  m_generator.setMessage(btjson::getTrackersForTorrent(hash));
  m_generator.setContentEncoding(m_parser.acceptsEncoding());
  write();
}

void HttpConnection::respondFilesPropertiesJson(const QString& hash) {
  m_generator.setStatusLine(200, "OK");
  m_generator.setContentTypeByExt("js");
  m_generator.setMessage(btjson::getFilesForTorrent(hash));
  m_generator.setContentEncoding(m_parser.acceptsEncoding());
  write();
}

void HttpConnection::respondPreferencesJson() {
  m_generator.setStatusLine(200, "OK");
  m_generator.setContentTypeByExt("js");
  m_generator.setMessage(prefjson::getPreferences());
  m_generator.setContentEncoding(m_parser.acceptsEncoding());
  write();
}

void HttpConnection::respondGlobalTransferInfoJson() {
  m_generator.setStatusLine(200, "OK");
  m_generator.setContentTypeByExt("js");
  m_generator.setMessage(btjson::getTransferInfo());
  m_generator.setContentEncoding(m_parser.acceptsEncoding());
  write();
}

void HttpConnection::respondCommand(const QString& command) {
  qDebug() << Q_FUNC_INFO << command;
  if (command == "download") {
    QString urls = m_parser.post("urls");
    QStringList list = urls.split('\n');
    foreach (QString url, list) {
      url = url.trimmed();
      if (!url.isEmpty()) {
        if (url.startsWith("bc://bt/", Qt::CaseInsensitive)) {
          qDebug("Converting bc link to magnet link");
          url = misc::bcLinkToMagnet(url);
        }
        if (url.startsWith("magnet:", Qt::CaseInsensitive)) {
          emit MagnetReadyToBeDownloaded(url);
        } else {
          qDebug("Downloading url: %s", qPrintable(url));
          emit UrlReadyToBeDownloaded(url);
        }
      }
    }
    return;
  }

  if (command == "addTrackers") {
    QString hash = m_parser.post("hash");
    if (!hash.isEmpty()) {
      QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
      if (h.is_valid() && h.has_metadata()) {
        QString urls = m_parser.post("urls");
        QStringList list = urls.split('\n');
        foreach (const QString& url, list) {
          announce_entry e(url.toStdString());
          h.add_tracker(e);
        }
      }
    }
    return;
  }
  if (command == "upload") {
    qDebug() << Q_FUNC_INFO << "upload";
    const QList<QByteArray>& torrents = m_parser.torrents();
    foreach(const QByteArray& torrentContent, torrents) {
      // Get a unique filename
      QTemporaryFile *tmpfile = new QTemporaryFile(QDir::temp().absoluteFilePath("qBT-XXXXXX.torrent"));
      tmpfile->setAutoRemove(false);
      if (tmpfile->open()) {
        QString filePath = tmpfile->fileName();
        tmpfile->write(torrentContent);
        tmpfile->close();
        // XXX: tmpfile needs to be deleted on Windows before using the file
        // or it will complain that the file is used by another process.
        delete tmpfile;
        emit torrentReadyToBeDownloaded(filePath, false, QString(), false);
        // Clean up
        fsutils::forceRemove(filePath);
      } else {
        std::cerr << "I/O Error: Could not create temporary file" << std::endl;
        delete tmpfile;
        return;
      }
    }
    // Prepare response
    m_generator.setStatusLine(200, "OK");
    m_generator.setContentTypeByExt("html");
    m_generator.setMessage(QString("<script type=\"text/javascript\">window.parent.hideAll();</script>"));
    m_generator.setContentEncoding(m_parser.acceptsEncoding());
    write();
    return;
  }
  if (command == "resumeall") {
    emit resumeAllTorrents();
    return;
  }
  if (command == "pauseall") {
    emit pauseAllTorrents();
    return;
  }
  if (command == "resume") {
    emit resumeTorrent(m_parser.post("hash"));
    return;
  }
  if (command == "setPreferences") {
    prefjson::setPreferences(m_parser.post("json"));
    return;
  }
  if (command == "setFilePrio") {
    QString hash = m_parser.post("hash");
    int file_id = m_parser.post("id").toInt();
    int priority = m_parser.post("priority").toInt();
    QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
    if (h.is_valid() && h.has_metadata()) {
      h.file_priority(file_id, priority);
    }
    return;
  }
  if (command == "getGlobalUpLimit") {
    m_generator.setStatusLine(200, "OK");
    m_generator.setContentTypeByExt("html");
    m_generator.setMessage(QByteArray::number(QBtSession::instance()->getSession()->settings().upload_rate_limit));
    m_generator.setContentEncoding(m_parser.acceptsEncoding());
    write();
    return;
  }
  if (command == "getGlobalDlLimit") {
    m_generator.setStatusLine(200, "OK");
    m_generator.setContentTypeByExt("html");
    m_generator.setMessage(QByteArray::number(QBtSession::instance()->getSession()->settings().download_rate_limit));
    m_generator.setContentEncoding(m_parser.acceptsEncoding());
    write();
    return;
  }
  if (command == "getTorrentUpLimit") {
    QString hash = m_parser.post("hash");
    QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
    if (h.is_valid()) {
      m_generator.setStatusLine(200, "OK");
      m_generator.setContentTypeByExt("html");
      m_generator.setMessage(QByteArray::number(h.upload_limit()));
      m_generator.setContentEncoding(m_parser.acceptsEncoding());
      write();
    }
    return;
  }
  if (command == "getTorrentDlLimit") {
    QString hash = m_parser.post("hash");
    QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
    if (h.is_valid()) {
      m_generator.setStatusLine(200, "OK");
      m_generator.setContentTypeByExt("html");
      m_generator.setMessage(QByteArray::number(h.download_limit()));
      m_generator.setContentEncoding(m_parser.acceptsEncoding());
      write();
    }
    return;
  }
  if (command == "setTorrentUpLimit") {
    QString hash = m_parser.post("hash");
    qlonglong limit = m_parser.post("limit").toLongLong();
    if (limit == 0) limit = -1;
    QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
    if (h.is_valid()) {
      h.set_upload_limit(limit);
    }
    return;
  }
  if (command == "setTorrentDlLimit") {
    QString hash = m_parser.post("hash");
    qlonglong limit = m_parser.post("limit").toLongLong();
    if (limit == 0) limit = -1;
    QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
    if (h.is_valid()) {
      h.set_download_limit(limit);
    }
    return;
  }
  if (command == "setGlobalUpLimit") {
    qlonglong limit = m_parser.post("limit").toLongLong();
    if (limit == 0) limit = -1;
    QBtSession::instance()->setUploadRateLimit(limit);
    Preferences::instance()->setGlobalUploadLimit(limit/1024.);
    return;
  }
  if (command == "setGlobalDlLimit") {
    qlonglong limit = m_parser.post("limit").toLongLong();
    if (limit == 0) limit = -1;
    QBtSession::instance()->setDownloadRateLimit(limit);
    Preferences::instance()->setGlobalDownloadLimit(limit/1024.);
    return;
  }
  if (command == "pause") {
    emit pauseTorrent(m_parser.post("hash"));
    return;
  }
  if (command == "delete") {
    QStringList hashes = m_parser.post("hashes").split("|");
    foreach (const QString &hash, hashes) {
      emit deleteTorrent(hash, false);
    }
    return;
  }
  if (command == "deletePerm") {
    QStringList hashes = m_parser.post("hashes").split("|");
    foreach (const QString &hash, hashes) {
      emit deleteTorrent(hash, true);
    }
    return;
  }
  if (command == "increasePrio") {
    increaseTorrentsPriority(m_parser.post("hashes").split("|"));
    return;
  }
  if (command == "decreasePrio") {
    decreaseTorrentsPriority(m_parser.post("hashes").split("|"));
    return;
  }
  if (command == "topPrio") {
    foreach (const QString &hash, m_parser.post("hashes").split("|")) {
      QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
      if (h.is_valid()) h.queue_position_top();
    }
    return;
  }
  if (command == "bottomPrio") {
    foreach (const QString &hash, m_parser.post("hashes").split("|")) {
      QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
      if (h.is_valid()) h.queue_position_bottom();
    }
    return;
  }
  if (command == "recheck") {
    QBtSession::instance()->recheckTorrent(m_parser.post("hash"));
    return;
  }
}

void HttpConnection::decreaseTorrentsPriority(const QStringList &hashes) {
  qDebug() << Q_FUNC_INFO << hashes;
  std::priority_queue<QPair<int, QTorrentHandle>,
      std::vector<QPair<int, QTorrentHandle> >,
      std::less<QPair<int, QTorrentHandle> > > torrent_queue;
  // Sort torrents by priority
  foreach (const QString &hash, hashes) {
    try {
      QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
      if (!h.is_seed()) {
        torrent_queue.push(qMakePair(h.queue_position(), h));
      }
    }catch(invalid_handle&) {}
  }
  // Decrease torrents priority (starting with the ones with lowest priority)
  while(!torrent_queue.empty()) {
    QTorrentHandle h = torrent_queue.top().second;
    try {
      h.queue_position_down();
    } catch(invalid_handle& h) {}
    torrent_queue.pop();
  }
}

void HttpConnection::increaseTorrentsPriority(const QStringList &hashes)
{
  qDebug() << Q_FUNC_INFO << hashes;
  std::priority_queue<QPair<int, QTorrentHandle>,
      std::vector<QPair<int, QTorrentHandle> >,
      std::greater<QPair<int, QTorrentHandle> > > torrent_queue;
  // Sort torrents by priority
  foreach (const QString &hash, hashes) {
    try {
      QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash);
      if (!h.is_seed()) {
        torrent_queue.push(qMakePair(h.queue_position(), h));
      }
    }catch(invalid_handle&) {}
  }
  // Increase torrents priority (starting with the ones with highest priority)
  while(!torrent_queue.empty()) {
    QTorrentHandle h = torrent_queue.top().second;
    try {
      h.queue_position_up();
    } catch(invalid_handle& h) {}
    torrent_queue.pop();
  }
}
