/* $Id: proxyguess.cc,v 1.8 2003/09/27 21:31:04 atterer Exp $ -*- C++ -*-
  __   _
  |_) /|  Copyright (C) 2003  |  richard@
  | \/¯|  Richard Atterer     |  atterer.net
  ¯ '` ¯
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License, version 2. See
  the file COPYING for details.

  Find proxy URLs by reading config files of various browsers.

  Warning, this code probably tries to be too clever: It compares the "last
  modified" timestamps of the various config files and chooses the
  configuration with the most recent timestamp.

*/

#include <config.h>

#include <fstream>
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <glibwww.hh>
#include <log.hh>
#include <proxyguess.hh>
//______________________________________________________________________

DEBUG_UNIT("proxyguess")

#if WINDOWS

#include <windows.h>

/* Windows: Read Internet Explorer's proxy settings. Doesn't work with .pac
   files, just with user-supplied servers. */
namespace {

  void proxyGuess_MSIE(HKEY internetSettings) {
    const unsigned BUFLEN = 256;
    DWORD len = BUFLEN;
    byte buf[BUFLEN];
    DWORD type;

    if (RegQueryValueEx(internetSettings, "ProxyEnable", NULL, &type,
                        buf, &len) == ERROR_SUCCESS
        && type == REG_BINARY && buf[0] == 0) {
      // User deselected the option "Use a proxy server", so don't continue
      debug("No proxies set up");
      return;
    }

    len = BUFLEN;
    // List of servers not to use the proxy for
    if (RegQueryValueEx(internetSettings, "ProxyOverride", NULL, &type,
                        buf, &len) == ERROR_SUCCESS
        && type == REG_SZ && buf[0] != 0) {
      // String has the form "lan;<local>". Split at ; and ignore <local>
      string host;
      const char* list = reinterpret_cast<const char*>(buf);
      while (*list != '\0') {
        if (!(isalnum(*list) || *list == '.' || *list == '-')) {
          ++list;
          continue;
        }
        while (isalnum(*list) || *list == '.' || *list == '-') {
          host += *list;
          ++list;
        }
        if (host != "local") {
          debug("No proxy for %1", host);
          glibwww_add_noproxy(host.c_str());
        }
        host.erase();
      }
    }

    len = BUFLEN;
    if (RegQueryValueEx(internetSettings, "ProxyServer", NULL, &type,
                        buf, &len) == ERROR_SUCCESS
        && type == REG_SZ && len >= 2 && buf[0] != 0) {
      /* String has one of two formats. Either simple format, one proxy for
         all: "ox:8080", or per-protocol format:
         "ftp=ox:8081;gopher=ox:8080;http=ox:8080;https=ox:8080" */
      string entry;
      const char* list = reinterpret_cast<const char*>(buf);
      while (true) {
        while (*list != '\0' && *list != ';') {
          entry += *list;
          ++list;
        }
        string::size_type equals = entry.find('=');
        if (equals == string::npos) {
          // Simple proxy setting, assume it's both for HTTP and FTP
          string proxy = "http://";
          proxy.append(reinterpret_cast<const char*>(buf));
          debug("General proxy: %1", proxy);
          glibwww_add_proxy("http", proxy.c_str());
          glibwww_add_proxy("ftp", proxy.c_str());
        } else {
          // Per-protocol proxy settings
          string proto(entry, 0, equals);
          if (proto == "http" || proto == "ftp") {
            string proxy = "http://";
            proxy.append(entry, equals + 1, string::npos);
            debug("%1 proxy: %2", proto, proxy);
            glibwww_add_proxy(proto.c_str(), proxy.c_str());
          }
        }
        entry.erase();
        if (*list == '\0') break;
        ++list;
      } // endwhile (true)
    } // endif (RegQueryValueEx("ProxyServer") == ERROR_SUCCESS)

  }
}

void proxyGuess() {
  HKEY software;
  if (RegOpenKeyEx(HKEY_CURRENT_USER, "Software", 0, KEY_READ, &software)
      == ERROR_SUCCESS) {
    HKEY microsoft;
    if (RegOpenKeyEx(software, "Microsoft", 0, KEY_READ, &microsoft)
        == ERROR_SUCCESS) {
      HKEY windows;
      if (RegOpenKeyEx(microsoft, "Windows", 0, KEY_READ, &windows)
          == ERROR_SUCCESS) {
        HKEY currentVersion;
        if (RegOpenKeyEx(windows, "CurrentVersion", 0, KEY_READ,
                         &currentVersion) == ERROR_SUCCESS) {
          HKEY internetSettings;
          if (RegOpenKeyEx(currentVersion, "Internet Settings", 0,
                           KEY_READ, &internetSettings) == ERROR_SUCCESS) {
            proxyGuess_MSIE(internetSettings);
            RegCloseKey(internetSettings);
          }
          RegCloseKey(currentVersion);
        }
        RegCloseKey(windows);
      }
      RegCloseKey(microsoft);
    }
    RegCloseKey(software);
  }
}
//______________________________________________________________________

#else

// Proxy guess code for Unix

namespace {

  struct BrowserConfig {
    /* Init filename and timestamp */
    BrowserConfig(const string& name, time_t timestamp);
    void init(time_t timestamp);
    virtual ~BrowserConfig();
    /* Reads config file, returns true if proxies could be set. */
    virtual bool read() = 0;

    /* In order for things to work with time_t wraparound, record one recent
       timestamp "now" and always work with the "age" of files in seconds. */
    static time_t now;
    string filename;
    signed age; // Allow future dates, e.g. happens with NFS + clock skew
  };

  BrowserConfig::BrowserConfig(const string& name, time_t timestamp) {
    if (now == 0) time(&now);
    filename = name;
    age = now - timestamp;
  }

  BrowserConfig::~BrowserConfig() { }

  time_t BrowserConfig::now = 0;

  // Compare pointers to BrowserConfigs by comparing the objects' age
  struct ConfFilesCompare {
    bool operator()(const BrowserConfig* a, const BrowserConfig* b) {
      return a->age < b->age;
    }
  };

}
//______________________________________________________________________

namespace {

# ifndef TESTING_PROXYGUESS
  // Return the last modification date of the file in question, 0 on error
  inline time_t fileModTime(const char* path) {
    struct stat fileInfo;
    if (stat(path, &fileInfo) != 0) return 0;
    return fileInfo.st_mtime;
  }
  typedef ifstream MyIfstream;
# endif

  // Add file info to set of candidate config files
  template<class Browser>
  void add(set<BrowserConfig*, ConfFilesCompare>* c, const char* name) {
    if (name == 0) return;
    time_t timestamp = fileModTime(name);
    if (timestamp == 0) return;
    c->insert(new Browser(string(name), timestamp));
  }

  template<class Browser>
  void add(set<BrowserConfig*, ConfFilesCompare>* c, const string& name) {
    time_t timestamp = fileModTime(name.c_str());
    if (timestamp == 0) return;
    c->insert(new Browser(name, timestamp));
  }

  // E.g. protocol == "http", proxy == "http://localhost:8080/"
  // NB proxy must start with a scheme like "http:"
  inline bool addProxy(const char* protocol, const char* proxy) {
    while (*proxy == ' ' || *proxy == '\t' || *proxy == '=') ++proxy;
    if (*proxy == '#') return false; // Assuming comment, not setting proxy
    if (*proxy == '\0') return false;
    debug("%1 proxy: %2", protocol, proxy);
    glibwww_add_proxy(protocol, proxy);
    return true;
  }

  void addNoProxy(const char* list) {
    string host;
    while (*list != '\0') {
      if (*list == '#') return; // Assuming start of comment
      if (!(isalnum(*list) || *list == '.' || *list == '-')) {
        ++list;
        continue;
      }
      while (isalnum(*list) || *list == '.' || *list == '-') {
        host += *list;
        ++list;
      }
      debug("No proxy for %1", host);
      glibwww_add_noproxy(host.c_str());
      host.erase();
    }
  }

} // namespace
//______________________________________________________________________

namespace {

  struct Lynx : BrowserConfig {
    Lynx(const string& n, time_t t) : BrowserConfig(n, t) { }
    bool read();
  };

  bool Lynx::read() {
    bool result = false;
    static const string whitespace = " \t";
    MyIfstream s(filename.c_str());
    string line;
    while (s) {
      getline(s, line);
      string::size_type pos = line.find_first_not_of(whitespace);
      if (pos == string::npos) continue;
      const char* l = line.c_str();
      if (strncmp(l + pos, "http_proxy:", 11) == 0) {
        if (addProxy("http", l + 11)) result = true;
      } else if (strncmp(l + pos, "ftp_proxy:", 10) == 0) {
        if (addProxy("ftp", l + 10)) result = true;
      } else if (strncmp(l + pos, "no_proxy:", 9) == 0) {
        addNoProxy(l + 9);
      }
    }
    return result;
  }

}
//______________________________________________________________________

namespace {

  struct Wget : BrowserConfig {
    Wget(const string& n, time_t t) : BrowserConfig(n, t) { }
    bool read();
  };

  bool Wget::read() {
    // Maybe this should react on "use_proxy = off", but it doesn't
    bool result = false;
    static const string whitespace = " \t";
    MyIfstream s(filename.c_str());
    string line;
    while (s) {
      getline(s, line);
      string::size_type pos = line.find_first_not_of(whitespace);
      if (pos == string::npos) continue;
      const char* l = line.c_str();
      if (strncmp(l + pos, "http_proxy", 10) == 0) {
        if (addProxy("http", l + 10)) result = true;
      } else if (strncmp(l + pos, "ftp_proxy", 9) == 0) {
        if (addProxy("ftp", l + 9)) result = true;
      } else if (strncmp(l + pos, "no_proxy", 8) == 0) {
        addNoProxy(l + 8);
      }
    }
    return result;
  }

}
//______________________________________________________________________

namespace {

  struct Mozilla : BrowserConfig {
    Mozilla(const string& n, time_t t) : BrowserConfig(n, t) { }
    bool read();
    void setString(string* dest, const char* src);
  };

  void Mozilla::setString(string* dest, const char* src) {
    dest->erase();
    while (*src == '"' || *src == ' ' || *src == '\t' || *src == ',') ++src;
    while (*src != '\0' && *src != ')' && *src != '"') {
      *dest += *src;
      ++src;
    }
  }

  // Works for Netscape 4.7, Galeon, Mozilla 5/6
  bool Mozilla::read() {
    // Doesn't understand .pac proxy definitions
    MyIfstream s(filename.c_str());
    string line, ftphost, ftpport, httphost, httpport, noproxy;
    while (s) {
      getline(s, line);
      const char USERPREF[] = "user_pref(\"network.proxy.";
      const char PREF[] = "pref(\"network.proxy.";
      const char* l = line.c_str();
      if (strncmp(l, USERPREF, sizeof(USERPREF)-1) == 0)
        l += sizeof(USERPREF)-1;
      else if (strncmp(l, PREF, sizeof(PREF)-1) == 0)
        l += sizeof(PREF)-1;
      else
        continue;
      if (strncmp(l, "ftp\"", 4) == 0)
        setString(&ftphost, l + 4);
      else if (strncmp(l, "ftp_port\"", 9) == 0)
        setString(&ftpport, l + 9);
      else if (strncmp(l, "http\"", 5) == 0)
        setString(&httphost, l + 5);
      else if (strncmp(l, "http_port\"", 10) == 0)
        setString(&httpport, l + 10);
      else if (strncmp(l, "no_proxies_on\"", 14) == 0)
        setString(&noproxy, l + 14);
    }

    bool result = false;
    if (!httphost.empty() && !httpport.empty()) {
      httphost.insert(0, "http://");
      httphost += ':';
      httphost += httpport;
      if (addProxy("http", httphost.c_str())) result = true;
    }
    if (!ftphost.empty() && !ftpport.empty()) {
      ftphost.insert(0, "http://");
      ftphost += ':';
      ftphost += ftpport;
      if (addProxy("ftp", ftphost.c_str())) result = true;
    }
    if (!noproxy.empty()) addNoProxy(noproxy.c_str());
    return result;
  }

}
//______________________________________________________________________

namespace {

  struct KDE : BrowserConfig {
    KDE(const string& n, time_t t) : BrowserConfig(n, t) { }
    bool read();
  };

  // Reads kioslaverc
  bool KDE::read() {
    // Maybe this should react on "use_proxy = off", but it doesn't
    bool result = false;
    static const string whitespace = " \t";
    MyIfstream s(filename.c_str());
    string line;
    while (s) {
      getline(s, line);
      string::size_type pos = line.find_first_not_of(whitespace);
      if (pos == string::npos) continue;
      const char* l = line.c_str();
      if (strncmp(l + pos, "httpProxy", 9) == 0) {
        if (addProxy("http", l + 9)) result = true;
      } else if (strncmp(l + pos, "ftpProxy", 8) == 0) {
        if (addProxy("ftp", l + 8)) result = true;
      } else if (strncmp(l + pos, "NoProxyFor", 10) == 0) {
        addNoProxy(l + 10);
      }
    }
    return result;
  }

}
//______________________________________________________________________

void proxyGuess() {
  string home = g_get_home_dir();
  if (home[home.size() - 1] != DIRSEP) home += DIRSEP;
  //____________________

  typedef set<BrowserConfig*, ConfFilesCompare> ConfFiles;
  ConfFiles c;

  add<Lynx>(&c, "/etc/lynx.cfg");
  add<Lynx>(&c, home + ".lynxrc");
  add<Lynx>(&c, getenv("LYNX_CFG"));

  add<Wget>(&c, "/etc/wgetrc");
  add<Wget>(&c, home + ".wgetrc");
  add<Wget>(&c, getenv("WGETRC"));

  add<Mozilla>(&c, "/etc/netscape4/defaults/preferences.js");
  add<Mozilla>(&c, "/etc/mozilla/prefs.js");
  add<Mozilla>(&c, home + ".netscape/preferences.js");
  add<Mozilla>(&c, home + ".galeon/mozilla/galeon/prefs.js");
  add<Mozilla>(&c, home + ".mozilla/default/prefs.js");
  add<Mozilla>(&c, home + ".netscape6/default/prefs.js"); // Is this correct?

  add<KDE>(&c, home + ".kde/share/config/kioslaverc");

  bool finished = false;
  while (!finished && !c.empty()) {
    ConfFiles::iterator first = c.begin();
    ConfFiles::iterator second = first;
    ++second;
    BrowserConfig* bc = *first;
    debug("Scan: %1  %2", bc->age, bc->filename);
    finished = bc->read();
    delete bc;
    c.erase(first, second);
  }
  // Most recent browser config was found, just delete rest
  while (!c.empty()) {
    ConfFiles::iterator first = c.begin();
    ConfFiles::iterator second = first;
    ++second;
    BrowserConfig* bc = *first;
    debug("Ignr: %1  %2", bc->age, bc->filename);
    delete bc;
    c.erase(first, second);
  }

}

#endif /* WINDOWS */
