/***************************************************************************
**
**  This file is part of QGpGuiTools.
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 of the License, or (at your option) any later version.
**
**  This file 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 Lesser General Public
**  License for more details.
**
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2011-01-04
**  Copyright: 2011-2019
**    Marc Wathelet (ISTerre, Grenoble, France)
**
***************************************************************************/

#include <QtNetwork>

#ifdef Q_OS_WIN
#  include <qt_windows.h>
#endif

#include "HttpProxyList.h"
#include "HttpProxyScript.h"

namespace QGpGuiTools {

  /*!
    \class HttpProxyList HttpProxyList.h
    \brief Brief description of class still missing

    Full description of class still missing
  */

  /*!
    \a testUrl is used for solving PAC scripts. It must corresponds to the expected host name
    of all requests to be sent to HttpAccess.
  */
  HttpProxyList::HttpProxyList(const QString& testUrl, QObject * parent)
    : QObject(parent), _testUrl(testUrl)
  {
    TRACE;
    _atHome=false;
    // Clean up of past releases
    QStringList filter;
    filter.append("geopsy.org.*.*.proxy.debug");
    QDir d=QDir::temp();
    QStringList files=d.entryList(filter, QDir::Files);
    foreach(QString file, files) {
      d.remove(file);
    }
  }

  /*!
    Description of destructor still missing
  */
  HttpProxyList::~HttpProxyList()
  {
    TRACE;
  }

  void HttpProxyList::collect()
  {
    TRACE;
    QString proxy=getenv("http_proxy");
    if(proxy.isEmpty()) {
      _proxies.append(HttpProxy());
    } else {
      _proxies.append(HttpProxy(proxy));
    }
    //printDebug("environment:\n", true);
    // TODO: check with Google Chromium for all platforms
    firefoxProxies();
    //printDebug("firefox:\n", true);
#if defined Q_OS_LINUX
    kdeProxies();
    //printDebug("kde:\n", true);
#elif defined Q_OS_WIN
    internetExplorerProxies();
    //printDebug("ie:\n", true);
#elif defined Q_OS_MAC
    macProxies();
    //printDebug("mac:\n", true);
#endif
    // Add other proxy sources supported directly by Qt
    systemProxies();
    //printDebug("system:\n", true);

    if(_replies.isEmpty()) { // No script or auto
      sort();
    }
  }

  /*!
    The list of proxies is established
  */
  void HttpProxyList::sort()
  {
    TRACE;
    // Clean duplicate entries
    //printDebug("final list (unsorted):\n", true);
    std::sort(_proxies.begin(), _proxies.end());
    unique(_proxies);
    //printDebug("final list (sorted):\n", true);
    // At home (Univerte Grenoble Alpes), test url can be accessed without proxy
    // but not the rest of the world
    if(_proxies.contains(HttpProxy("www-cache.ujf-grenoble.fr:3128"))) {
      _atHome=true;
    }
    emit ready();
  }

  void HttpProxyList::replyReceived(QNetworkReply * reply)
  {
    TRACE;
    int i=_replies.indexOf(reply);
    if(i>-1) {
      _replies.removeAt(i);
      if(_replies.isEmpty()) {
        sort();
      }
    }
  }

  void HttpProxyList::getScript(const QString& url)
  {
    TRACE;
    QNetworkAccessManager * manager=new QNetworkAccessManager(this);
    QNetworkReply * reply=manager->get(QNetworkRequest(url));
    connect(reply, SIGNAL(finished()), this, SLOT(getScriptFinished()));
    _replies.append(reply);
    //printDebug(QString("get script at %1\n").arg(url));
  }

  void HttpProxyList::getScriptFinished()
  {
    TRACE;
    //printDebug(QString("get script finished, %1 replies\n").arg(_replies.count()));
    QNetworkReply * reply=static_cast<QNetworkReply *>(sender());
    if(reply->error()==QNetworkReply::NoError) {
      //printDebug(QString("get script successful\n"));
      HttpProxyScript s(reply->readAll());
      _proxies.append(s.proxies(_testUrl));
    }
    replyReceived(reply);
  }

  void HttpProxyList::dnsDetectRequest(const QString& domainName)
  {
    TRACE;
    QUrl wpad(QString("http://wpad%1/wpad.dat").arg(domainName));
    //printDebug(QString("wpad %1\n").arg(wpad.toString()));
    QNetworkAccessManager * manager=new QNetworkAccessManager(this);
    QNetworkReply * reply=manager->get(QNetworkRequest(wpad));
    connect(reply, SIGNAL(finished()), this, SLOT(getScriptFinished()));
    _replies.append(reply);
  }

  /*!
      The user agent first tries a DHCPINFORM message. The reply to this message contains the URL of the configuration script.
      If that doesn't work, a DNS lookup for a well-known alias, WPAD, is issued. Then the configuration script's location is
      determined as http://<machine>/wpad.dat where the machine name is the result of the DNS lookup. You can try this in your
      own network. If you open Internet Explorer and go to http://wpad/wpad.dat, you might see the configuration script.
  */
  void HttpProxyList::autoDetectProxies()
  {
    TRACE;
    // TODO: implement DHCPINFORM technique
    // DNS technique
    QStringList subDomains=QHostInfo::localDomainName().split(".");
    if(subDomains.count()>1) {
      QString domainName=subDomains.last();
      for(int i=subDomains.count()-2; i>=0; i--) {
        domainName=subDomains.at(i)+"."+domainName;
        dnsDetectRequest("."+domainName);
      }
    }
    dnsDetectRequest("");
  }

  /*!
    Returns a list of proxies registered in system preferences.
    It is based on Qt implementation QNetworkProxyFactory::systemProxyForQuery().
  */
  void HttpProxyList::systemProxies()
  {
    TRACE;
    QList<QNetworkProxy> list=QNetworkProxyFactory::systemProxyForQuery();
    foreach(const QNetworkProxy& p, list) {
      _proxies.append(HttpProxy(p));
    }
  }

  /*!
    Convenient function to search a pattern in a directory (recursive)
  */
  QStringList HttpProxyList::findFiles(QDir in, QString pattern)
  {
    TRACE;
    QStringList patternList;
    patternList.append(pattern);
    return findFiles(in, patternList);
  }

  /*!
    Recursive search of files in directory \a in
  */
  QStringList HttpProxyList::findFiles(QDir in, QStringList patternList)
  {
    TRACE;
    QStringList filePaths;
    QStringList subDirs=in.entryList(QDir::AllDirs);
    for(QStringList::iterator it=subDirs.begin(); it!=subDirs.end(); it++ ) {
      if(*it!="." && *it!="..") {
        QDir d(in.absoluteFilePath(*it));
        filePaths += findFiles(d, patternList);
      }
    }
    QStringList files=in.entryList(patternList, QDir::Files);
    for(QStringList::iterator it=files.begin(); it!=files.end(); it++ ) {
      filePaths.append(in.absoluteFilePath(*it));
    }
    return filePaths;
  }

  QStringList HttpProxyList::firefoxFiles()
  {
    TRACE;
    QDir d=QDir::home();
#if defined Q_OS_LINUX
    if(d.cd(".mozilla") && d.cd("firefox")) {
      return findFiles(d, "prefs.js" );
    } else {
      return QStringList();
    }
#elif defined Q_OS_WIN
    d.cd("Application Data");
    d.cd("Mozilla");
    d.cd("Firefox");
    return findFiles(d, "prefs.js" );
#elif defined Q_OS_MAC
    d.cd("Library");
    d.cd("Application Support");
    d.cd("Firefox");
    return findFiles(d, "prefs.js" );
  #endif
  }

  /*!
    Grabs information from Firefox settings
  */
  void HttpProxyList::firefoxProxies()
  {
    TRACE;
    QStringList prefFiles=firefoxFiles();
    for(QStringList::iterator it=prefFiles.begin(); it!=prefFiles.end(); it++) {
      firefoxProxies(*it);
    }
  }

  void HttpProxyList::firefoxProxies(const QString& fileName)
  {
    TRACE;
    QFile f(fileName);
    if(f.open(QIODevice::ReadOnly)) {
      QTextStream s(&f);
      QString content=s.readAll();

      QRegExp typeExp ("user_pref[(]\"network.proxy.type\", +([^)]*)[)];");
      if(typeExp.indexIn(content)>-1) {
        QNetworkProxy p;
        switch (typeExp.cap(1).toInt()) {
        case 0:
          _proxies.append(HttpProxy());
          break;
        case 1: { // Manual proxy
            p.setType(QNetworkProxy::HttpProxy);
            QRegExp hostExp ("user_pref[(]\"network.proxy.http\", +\"([^\"]*)\"[)];");
            if(hostExp.indexIn(content)>-1) {
              p.setHostName(hostExp.cap(1));
            }
            QRegExp portExp ("user_pref[(]\"network.proxy.http_port\", +([^)]*)[)];");
            if(portExp.indexIn(content)>-1) {
              p.setPort(portExp.cap(1).toInt());
            }
            _proxies.append(HttpProxy(p));
          }
          break;
        case 2: { // pac file
            QRegExp pacExp ("user_pref[(]\"network.proxy.autoconfig_url\", +\"([^\"]*)\"[)];");
            if(pacExp.indexIn(content)>-1) {
              getScript(pacExp.cap(1));
            }
          }
          break;
        case 4: // Automatic proxy detection...
          autoDetectProxies();
          break;
        default: // Bad value, do nothing
          break;
        }
      } else { // direct
        _proxies.append(HttpProxy());
      }
    }
  }


#ifdef Q_OS_LINUX
  void HttpProxyList::kdeProxies()
  {
    TRACE;
    QDir d=QDir::home();
    QStringList patternList;
    patternList.append(".kde*");
    QStringList kdeDirs=d.entryList(patternList, QDir::Dirs | QDir::Hidden);
    for(QStringList::iterator it=kdeDirs.begin(); it!=kdeDirs.end(); it++) {
      QFileInfo fi(d.absoluteFilePath(*it)+"/share/config/kioslaverc");
      if(fi.exists()) {
        kdeProxies(fi.filePath());
      }
    }
  }

  /*!
    .kde/share/config/kioslaverc
        [Proxy Settings]
        ProxyType=1 # manual proxy
                  3 # automatic proxy
                  0 # direct connection
                  2 # Auto pac
        Proxy Config Script=http://www.ujf-grenoble.fr/proxy-cache/auto-proxy.pac
        httpProxy=http://www-cache.ujf-grenoble.fr:3128
        NoProxyFor=
  */
  void HttpProxyList::kdeProxies(const QString& fileName)
  {
    TRACE;
    QSettings reg(fileName, QSettings::IniFormat);
    reg.beginGroup("Proxy Settings");
    switch(reg.value("ProxyType",0).toInt()) {
    case 0:    // Direct connection
      _proxies.append(HttpProxy());
      break;
    case 1:  // Manual proxy
      _proxies.append(HttpProxy(reg.value("httpProxy").toString()));
      break;
    case 2:    // Auto pac
      getScript(reg.value("Proxy Config Script").toString());
      break;
    case 3:    // Automatic proxy detection... (dns, dhcp methods)
      autoDetectProxies();
      break;
    default:   // Bad value, do nothing
      break;
    }
  }
#endif

#ifdef Q_OS_WIN
  /*
    Function copied from Qt qsettings_win.cpp, needed to access Windows registry directly.
    Open a key with the specified perms
  */
  static HKEY openKey(HKEY parentHandle, REGSAM perms, const QString &rSubKey)
  {
    HKEY resultHandle=0;
    LONG res=RegOpenKeyEx(parentHandle, reinterpret_cast<const wchar_t *>(rSubKey.utf16()),
                          0, perms, &resultHandle);
    if(res==ERROR_SUCCESS) return resultHandle;
    return 0;
  }

  QByteArray registryBinaryValue(HKEY parentHandle, QString rSubkeyPath, QString rSubkeyName, bool& ok)
  {
    QByteArray data;
    HKEY handle=openKey(parentHandle, KEY_READ, rSubkeyPath);
    if(handle==0) {
      ok=false;
      return data;
    }
    // get the size and type of the value
    DWORD dataType;
    DWORD dataSize;
    LONG res = RegQueryValueEx(handle, reinterpret_cast<const wchar_t *>(rSubkeyName.utf16()), 0, &dataType, 0, &dataSize);
    if (res != ERROR_SUCCESS) {
        RegCloseKey(handle);
        ok=false;
        return data;
    }

    // get the value
    data.resize(dataSize);
    res = RegQueryValueEx(handle, reinterpret_cast<const wchar_t *>(rSubkeyName.utf16()), 0, 0,
                           reinterpret_cast<unsigned char*>(data.data()), &dataSize);
    ok=(res==ERROR_SUCCESS);
    RegCloseKey(handle);
    return data;
  }

  /*!
    REG_BINARY key in registry stored in two ways:

    \li Per user: HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\DefaultConnectionSettings
    \li Global: HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\HttpProxyPerUser
                set to 0 then settings are in
                HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Connections\\DefaultConnectionSettings

    DefaultConnectionSettings is not necessarely the unique available connection other entries may exist with the same format

    Binary format:
      0000          3C 00 00 00  tag ????                     (ignored here)
      0004          6B 41 72 72  incremented at each change   (ignored here)
      0008  int     Type         pac=4, proxy=2, direct=1, autoProxy=8
      000C  int     nchars for proxy address:port
      0010  string  proxy address+port
            int     nchars for local filters
            string  local filter: "<local>" or "ftp=www-cache:3128;gopher=...;http=...;https=...;socks=..."
            int     nchars for auto pac
            string  auto pac address
            int     1 ????                                     (ignored here)
            24 int  0 values

    To be tested: GetProxy.exe http://www.example.com

    Reading REG_BINARY with QSettings is currently not possible, this will be fixed with Qt5
  */
  void HttpProxyList::internetExplorerProxies()
  {
    TRACE;
    static QString commonPath("Microsoft\\Windows\\CurrentVersion\\Internet Settings");
    bool ok;
    // First test if connections are defined for the machine or per user
    QByteArray data=registryBinaryValue(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\"+commonPath,
                                          "HttpProxyPerUser", ok);
    HKEY parentHandle;
    QString keyPath, qKeyPath;
    if(ok && data.toInt()==0) {
      parentHandle=HKEY_LOCAL_MACHINE;
      keyPath="SOFTWARE\\Policies\\"+commonPath+"\\connections";
      qKeyPath="HKEY_LOCAL_MACHINE\\"+keyPath;
    } else {
      parentHandle=HKEY_CURRENT_USER;
      keyPath="Software\\"+commonPath+"\\connections";
      qKeyPath="HKEY_CURRENT_USER\\"+keyPath;
    }
    // Get the list of keys under qKeyPath, we don't want to re-implement all Qt private framework to access Windows registry
    // Even if it is heavier, we prefer using QSettings
    QSettings * reg=new QSettings(qKeyPath, QSettings::NativeFormat);
    QStringList connections=reg->childKeys ();
    delete reg;
    for(QStringList::iterator it=connections.begin(); it!=connections.end();it++) {
      QByteArray data=registryBinaryValue(parentHandle, keyPath, *it, ok);
      if(ok) {
       // Extract usefull information from binary block
        int offset=0x0008;
        int type=*reinterpret_cast<qint32 *>(data.data()+offset);
        offset+=0x0004;
        int manualProxyCount=*reinterpret_cast<qint32 *>(data.data()+offset);
        offset+=0x0004;
        if(manualProxyCount+offset>data.count()) manualProxyCount=data.count()-offset;
        int manualProxy=offset;
        offset+=manualProxyCount;
        int localFilterCount=*reinterpret_cast<qint32 *>(data.data()+offset);
        offset+=0x0004;
        if(localFilterCount+offset>data.count()) localFilterCount=data.count()-offset;
        int localFilter=offset;
        offset+=localFilterCount;
        int pacCount=*reinterpret_cast<qint32 *>(data.data()+offset);
        offset+=0x0004;
        if(pacCount+offset>data.count()) pacCount=data.count()-offset;
        int pac=offset;

        if(type & 0x01) {                                 // Direct
          _proxies.append(HttpProxy());
        }
        if(type & 0x02) {                                 // Manual proxy
          _proxies.append(HttpProxy(QByteArray(data.data()+manualProxy, manualProxyCount)));
        }
        if(type & 0x04) {                                 // Auto pac
          getScript(QByteArray(data.data()+pac, pacCount));
        }
        if(type & 0x08) {                                 // Auto proxy
          autoDetectProxies();
        }
      }
    }
  }
#endif

#ifdef Q_OS_MAC
  /*!
  */
  void HttpProxyList::macProxies()
  {
    TRACE;
    QProcess p;
    p.start("scutil --proxy");
    p.setProcessChannelMode(QProcess::MergedChannels);
    if(p.waitForStarted(10000)) {
      while(!p.waitForFinished (10000)) {}
      QString content=p.readAll().data();
      QRegExp enableExp ("HTTPEnable *: *([01])");
      if(enableExp.indexIn(content)>-1 && enableExp.cap(1).toInt()==1) {
        QNetworkProxy p;
        p.setType(QNetworkProxy::HttpProxy);
        QRegExp hostExp ("HTTPProxy *: *([^ ][^\n]+)");
        if(hostExp.indexIn(content)>-1) {
          p.setHostName(hostExp.cap(1));
        }
        QRegExp portExp ("HTTPPort *: *([0-9]+)");
        if(portExp.indexIn(content)>-1) {
          p.setPort(portExp.cap(1).toInt());
        }
        _proxies.append(HttpProxy(p));
      }
      QRegExp enablePacExp ("ProxyAutoConfigEnable *: *([01])");
      if(enablePacExp.indexIn(content)>-1 && enablePacExp.cap(1).toInt()==1) {
        QRegExp pacExp ("ProxyAutoConfigURLString *: *([^ ][^\n]+)");
        if(pacExp.indexIn(content)>-1) {
          getScript(pacExp.cap(1));
        }
      }
      QRegExp enableAutoExp ("ProxyAutoDiscoveryEnable *: *([01])");
      if(enableAutoExp.indexIn(content)>-1 && enableAutoExp.cap(1).toInt()==1) {
        autoDetectProxies();
      }
    }
  }
#endif

  /*!
    For debug, prints the list of available proxies.
  */
  void HttpProxyList::printDebug(const QString& msg, bool list) const
  {
    TRACE;
    QDir d=QDir::temp();
    QFile f(d.absoluteFilePath("geopsy.org.%1.%2.proxy.debug")
            .arg(CoreApplication::applicationName())
            .arg(QCoreApplication::applicationPid()));
    if(f.open(QIODevice::Append)) {
      QTextStream s(&f);
      s << msg;
      if(list) {
        for(QList<HttpProxy>::ConstIterator it=_proxies.begin();it!=_proxies.end();it++) {
          it->printDebug(s);
        }
      }
    }
  }

} // namespace QGpGuiTools
