/* Windows automatic proxy settings resolver
 * Jiri Hruska <jirka@fud.cz>, 2012-2013
 * Released under the WTFPL license. */
 
#define UNICODE
#define _UNICODE
#include <string>
#include <Windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")


namespace detail {


struct WinHttpHandle {
  HINTERNET m_hInternet;

  WinHttpHandle(HANDLE hInternet = NULL)
    : m_hInternet(hInternet)
  {
  }

  WinHttpHandle& operator=(HINTERNET hInternet)
  {
    if (hInternet != m_hInternet) {
      if (m_hInternet)
        WinHttpCloseHandle(m_hInternet);
      m_hInternet = hInternet;
    }
    return *this;
  }

  operator HINTERNET()
  {
    return m_hInternet;
  }

  ~WinHttpHandle()
  {
    if (m_hInternet)
      WinHttpCloseHandle(m_hInternet);
  }
};

struct WinHttpCurrentUserIeProxyConfig : public WINHTTP_CURRENT_USER_IE_PROXY_CONFIG {
  WinHttpCurrentUserIeProxyConfig()
  {
    memset(this, 0, sizeof(*this));
  }

  ~WinHttpCurrentUserIeProxyConfig()
  {
    if (lpszAutoConfigUrl)
      GlobalFree(lpszAutoConfigUrl);
    if (lpszProxy)
      GlobalFree(lpszProxy);
    if (lpszProxyBypass)
      GlobalFree(lpszProxyBypass);
  }
};

struct WinHttpProxyInfo : public WINHTTP_PROXY_INFO {
  WinHttpProxyInfo()
  {
    memset(this, 0, sizeof(*this));
  }

  ~WinHttpProxyInfo()
  {
    if (lpszProxy)
      GlobalFree(lpszProxy);
    if (lpszProxyBypass)
      GlobalFree(lpszProxyBypass);
  }
};

class CrackedUrl {
  public:
    CrackedUrl()
    {
      m_scheme[0] = 0;
      m_hostname[0] = 0;
    }

    bool crack(LPCWSTR url)
    {
      // Loosely based on InternetCrackUrl(), which needs another .dll dependency and
      // including both <WinHttp.h> and <WinInet.h> is broken in Windows SDK (...).

      LPCWSTR ptr = wcschr(url, L':');
      if (!ptr || ptr - url >= _countof(m_scheme))
        return false;
      wcsncpy_s(m_scheme, url, ptr - url);
      CharLower(m_scheme);

      if (wcsncmp(ptr, L"://", 3) == 0)
        url = ptr + 3;
      else
        url = ptr + 1;

      ptr = wcschr(url, L'/');

      LPCWSTR ptr2 = wcschr(url, L'@');
      if (ptr2 && ptr2 < ptr)
        url = ptr2 + 1;

      ptr2 = wcschr(url, L':');
      if (ptr2 && ptr2 < ptr)
        ptr = ptr2;

      wcsncpy_s(m_hostname, url, ptr - url);
      CharLower(m_hostname);
      return true;
    }

    LPCWSTR scheme() const
    {
      return m_scheme;
    }

    LPCWSTR hostname() const
    {
      return m_hostname;
    }

  protected:
    WCHAR m_scheme[32];
    WCHAR m_hostname[256];
};

class ProxyChecker
{
  protected:
    struct StaticContext {
      CRITICAL_SECTION cs;
      WinHttpHandle hInternet;
      bool fullAutodetectionFailed;

      StaticContext()
        : fullAutodetectionFailed(false)
      {
        InitializeCriticalSection(&cs);
      }

      ~StaticContext()
      {
        DeleteCriticalSection(&cs);
      }
    };

  protected:
    static StaticContext s_context;
    LPCWSTR m_url;
    CrackedUrl m_urlInfo;
    std::wstring m_out;
    WINHTTP_AUTOPROXY_OPTIONS m_apo;

  protected:
    bool checkProxyBypassList(LPWSTR list)
    {
      // The proxy bypass list contains one or more server names separated by semicolons
      // or whitespace. The proxy bypass list can also contain the string "<local>" to
      // indicate that all local intranet sites are bypassed. Local intranet sites are
      // considered to be all servers that do not contain a period in their name.

      static const wchar_t separators[] = L"\t\r\n ;";
      wchar_t* context;
      for (wchar_t* tok = wcstok_s(list, separators, &context); tok; tok = wcstok_s(NULL, separators, &context)) {
        CharLower(tok);

        if (wcscmp(tok, L"<local>") == 0) {
          if (wcschr(m_urlInfo.hostname(), L'.') == NULL ||
              wcscmp(m_urlInfo.hostname(), L"127.0.0.1") == 0)
            return true;
          continue;
        }

        const wchar_t* mask = tok;
        const wchar_t* hostname = m_urlInfo.hostname();
        const wchar_t* backtrack = NULL;
        while (*mask && *hostname) {
          if (*mask == *hostname) {
            mask++;
            hostname++;
          } else if (*mask == L'*') {
            while (mask[1] == L'*')
              mask++;
            backtrack = mask++;
            while (*hostname && *mask != *hostname)
              hostname++;
          } else if (backtrack) {
            mask = backtrack;
          } else {
            break;
          }

          if (!*mask && *hostname && backtrack)
            mask = backtrack;
        }

        while(*mask == L'*')
          mask++;
        if (!*mask && !*hostname)
          return true;
      }
      return false;
    }

    bool getProxyFromList(LPWSTR list)
    {
      // The proxy server list contains one or more of the following strings
      // separated by semicolons or whitespace:
      //   ([<scheme>=][<scheme>"://"]<server>[":"<port>])

      static const wchar_t separators[] = L"\t\r\n ;";
      wchar_t* context;
      for (wchar_t* tok = wcstok_s(list, separators, &context); tok; tok = wcstok_s(NULL, separators, &context)) {
        // If there are multiple entries for different protocols, accept only the requested one.
        wchar_t* pos = wcschr(tok, L'=');
        if (pos) {
          *pos = L'\0';
          if (wcscmp(tok, m_urlInfo.scheme()) != 0)
            continue;
          tok = pos + 1;
        }

        // If there is protocol included in the proxy address, accept only "http://".
        pos = wcsstr(tok, L"://");
        if (pos) {
          if (wcsncmp(tok, L"http:", pos - tok + 1) != 0)
            continue;
          tok = pos + 3;
        }

        m_out = tok;
        return true;
      }

      return false;
    }

    bool checkBypassAndGetProxy(LPWSTR proxyBypassList, LPWSTR proxyList)
    {
      if (proxyBypassList && checkProxyBypassList(proxyBypassList))
        return true;
      if (proxyList && getProxyFromList(proxyList))
        return true;
      return false;
    }

    bool tryGetProxyForUrl()
    {
      EnterCriticalSection(&s_context.cs);
      if ((m_apo.dwFlags & WINHTTP_AUTOPROXY_AUTO_DETECT) && s_context.fullAutodetectionFailed) {
        LeaveCriticalSection(&s_context.cs);
        return false;
      }

      if (!s_context.hInternet)
        s_context.hInternet = WinHttpOpen(NULL, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);
      if (!s_context.hInternet) {
        LeaveCriticalSection(&s_context.cs);
        return false;
      }

      m_apo.lpvReserved = NULL;
      m_apo.dwReserved = 0;
      m_apo.fAutoLogonIfChallenged = FALSE;

      WinHttpProxyInfo pi;
      BOOL bResult = WinHttpGetProxyForUrl(s_context.hInternet, m_url, &m_apo, &pi);
      if (!bResult) {
        if (GetLastError() == ERROR_WINHTTP_AUTODETECTION_FAILED) {
          s_context.fullAutodetectionFailed = true;
        } else if (GetLastError() == ERROR_WINHTTP_LOGIN_FAILURE) {
          m_apo.fAutoLogonIfChallenged = TRUE;
          bResult = WinHttpGetProxyForUrl(s_context.hInternet, m_url, &m_apo, &pi);
        }
      }
      LeaveCriticalSection(&s_context.cs);
      if (!bResult)
        return false;

      if (pi.dwAccessType == WINHTTP_ACCESS_TYPE_NO_PROXY)
        return true;

      if (pi.dwAccessType == WINHTTP_ACCESS_TYPE_NAMED_PROXY) {
        if (checkBypassAndGetProxy(pi.lpszProxyBypass, pi.lpszProxy))
          return true;
      }

      return false;
    }

  public:
    bool process(LPCWSTR url)
    {
      m_out.clear();

      m_url = url;
      if (!m_urlInfo.crack(url))
        return false;

      WinHttpCurrentUserIeProxyConfig iecfg;
      if (!WinHttpGetIEProxyConfigForCurrentUser(&iecfg))
        return false;

      bool triedSomething = false;

      // If auto-config script is provided, try to use it.
      if (iecfg.lpszAutoConfigUrl) {
        triedSomething = true;
        m_apo.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL;
        m_apo.dwAutoDetectFlags = 0;
        m_apo.lpszAutoConfigUrl = iecfg.lpszAutoConfigUrl;
        if (tryGetProxyForUrl())
          return true;
      }

      // If automatic detection is enabled, try to use it.
      if (iecfg.fAutoDetect) {
        triedSomething = true;
        m_apo.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT;
        m_apo.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A;
        m_apo.lpszAutoConfigUrl = NULL;
        WinHttpProxyInfo pi;
        if (tryGetProxyForUrl())
          return true;
      }

      // If custom proxy servers are configured, try to use them.
      if (iecfg.lpszProxy) {
        triedSomething = true;
        if (checkBypassAndGetProxy(iecfg.lpszProxyBypass, iecfg.lpszProxy))
          return true;
      }

      // If nothing was enabled, direct mode should be used.
      if (!triedSomething)
        return true;

      // Something was tried but failed. Perhaps use direct mode, but only as fallback.
      return false;
    }

    LPCWSTR proxyAddr() const
    {
      return (m_out.empty() ? NULL : m_out.c_str());
    }
};

ProxyChecker::StaticContext ProxyChecker::s_context;

}

/**
 * Pass in an URL, get HTTP proxy "hostname:port" back, or false return value to not use any proxy.
 */
bool getProxyForUrl(LPCWSTR url, std::wstring& out)
{
  detail::ProxyChecker pc;
  if (!pc.process(url) || !pc.proxyAddr())
    return false;
  out = pc.proxyAddr();
  return true;
}



//=============================================================================



#include <vector>
#include <CommCtrl.h>
#pragma comment(lib, "comctl32.lib")
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

enum {
  IDC_CONFIG_REFRESH = 100,
  IDC_CONFIG_AUTO,
  IDC_CONFIG_SCRIPT,
  IDC_CONFIG_SCRIPT_ADDR,
  IDC_CONFIG_CUSTOM,
  IDC_CONFIG_CUSTOM_ADDR,
  IDC_CONFIG_CUSTOM_BYPASS,
  IDC_INPUT,
  IDC_RESOLVE,
  IDC_OUTPUT,
  IDC_OUTPUT_CLEAR
};

INT_PTR WINAPI dialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch(uMsg) {
    case WM_INITDIALOG:
      SetFocus(GetDlgItem(hwndDlg, IDC_INPUT));
      SendMessage(hwndDlg, WM_COMMAND, MAKEWPARAM(IDC_CONFIG_REFRESH, BN_CLICKED), 0);
      return FALSE;

    case WM_DESTROY:
      PostQuitMessage(0);
      return TRUE;

    case WM_CLOSE:
      DestroyWindow(hwndDlg);
      return TRUE;

    case WM_COMMAND:
      switch(LOWORD(wParam)) {
        case IDC_CONFIG_REFRESH: {
          detail::WinHttpCurrentUserIeProxyConfig iecfg;
          if (!WinHttpGetIEProxyConfigForCurrentUser(&iecfg)) {
            MessageBox(hwndDlg, L"WinHttpGetIEProxyConfigForCurrentUser() failed.", L"Error", MB_ICONERROR | MB_OK);
          } else {
            CheckDlgButton(hwndDlg, IDC_CONFIG_AUTO,          iecfg.fAutoDetect ? BST_CHECKED : BST_UNCHECKED);
            CheckDlgButton(hwndDlg, IDC_CONFIG_SCRIPT,        iecfg.lpszAutoConfigUrl ? BST_CHECKED : BST_UNCHECKED);
            SetDlgItemText(hwndDlg, IDC_CONFIG_SCRIPT_ADDR,   iecfg.lpszAutoConfigUrl ? iecfg.lpszAutoConfigUrl : L"(null)");
            CheckDlgButton(hwndDlg, IDC_CONFIG_CUSTOM,        iecfg.lpszProxy ? BST_CHECKED : BST_UNCHECKED);
            SetDlgItemText(hwndDlg, IDC_CONFIG_CUSTOM_ADDR,   iecfg.lpszProxy ? iecfg.lpszProxy : L"(null)");
            SetDlgItemText(hwndDlg, IDC_CONFIG_CUSTOM_BYPASS, iecfg.lpszProxyBypass ? iecfg.lpszProxyBypass : L"(null)");
          }
          break;
        }

        case IDC_RESOLVE: {
          SendMessage(hwndDlg, WM_COMMAND, MAKEWPARAM(IDC_CONFIG_REFRESH, BN_CLICKED), 0);

          WCHAR url[1024];
          GetDlgItemText(hwndDlg, IDC_INPUT, url, _countof(url));

          wchar_t* context;
          for (wchar_t* tok = wcstok_s(url, L" \t\r\n", &context); tok; tok = wcstok_s(NULL, L" \t\r\n", &context)) {
            std::wstring proxy;
            bool success;
            {
              detail::ProxyChecker pc;
              success = pc.process(tok);
              if (success && pc.proxyAddr())
                proxy = pc.proxyAddr();
            }

            WCHAR tmp[1024];
            swprintf_s(tmp, L"%s -> %s\n", tok, success ? (proxy.empty() ? L"<direct>" : proxy.c_str()) : L"<failed>");
            HWND hwndOutput = GetDlgItem(hwndDlg, IDC_OUTPUT);
            int pos = GetWindowTextLength(hwndOutput);
            SendMessage(hwndOutput, EM_SETSEL, pos, pos);
            SendMessage(hwndOutput, EM_REPLACESEL, FALSE, reinterpret_cast<LPARAM>(tmp));
          }
          break;
        }

        case IDC_OUTPUT_CLEAR:
          SetDlgItemText(hwndDlg, IDC_OUTPUT, L"");
          break;
      }
      return TRUE;
  }

  return FALSE;
}

class DialogTemplate {
  public:
    enum Classes {
      BUTTON    = 0x0080,
      EDIT      = 0x0081,
      STATIC    = 0x0082,
      LISTBOX   = 0x0083,
      SCROLLBAR = 0x0084,
      COMBOBOX  = 0x0085
    };

  public:
    DialogTemplate(DWORD dwStyle, DWORD dwExtendedStyle, short x, short y, short cx, short cy, LPCWSTR lpszTitle)
    {
      size_t len = wcslen(lpszTitle) + 1;
      m_buffer.resize(sizeof(DLGTEMPLATE) + 2 + 2 + 2 * len);

      DLGTEMPLATE* dlg = reinterpret_cast<DLGTEMPLATE*>(&m_buffer[0]);
      dlg->style = dwStyle & ~DS_SETFONT;
      dlg->dwExtendedStyle = dwExtendedStyle;
      dlg->cdit = 0;
      dlg->x = x;
      dlg->y = y;
      dlg->cx = cx;
      dlg->cy = cy;

      WORD* meta = reinterpret_cast<WORD*>(&m_buffer[sizeof(DLGTEMPLATE)]);
      meta[0] = 0x0000;
      meta[1] = 0x0000;
      memcpy(meta + 2, lpszTitle, 2 * len);
    }

    void setFont(WORD wFontSize, LPCWSTR lpszFontFace)
    {
      size_t ofs = sizeof(DLGTEMPLATE) + 2 + 2 + 2 * (wcslen(reinterpret_cast<WCHAR*>(&m_buffer[sizeof(DLGTEMPLATE) + 2 + 2])) + 1);
      size_t len = wcslen(lpszFontFace) + 1;
      m_buffer.resize(ofs + 2 + 2 * len);

      DLGTEMPLATE* dlg = reinterpret_cast<DLGTEMPLATE*>(&m_buffer[0]);
      dlg->style |= DS_SETFONT;
      dlg->cdit = 0;

      WORD* meta = reinterpret_cast<WORD*>(&m_buffer[ofs]);
      meta[0] = wFontSize;
      memcpy(meta + 1, lpszFontFace, 2 * len);
    }

    void addControl(WORD wId, WORD wClassId, short x, short y, short cx, short cy, DWORD dwStyle, DWORD dwExtendedStyle, LPCWSTR lpszTitle)
    {
      size_t pad = (m_buffer.size() & 3) ? 2 : 0;
      size_t ofs = m_buffer.size() + pad;
      size_t len = wcslen(lpszTitle) + 1;
      m_buffer.resize(ofs + sizeof(DLGITEMTEMPLATE) + 2 + 2 + 2 * len + 2);

      DLGTEMPLATE* dlg = reinterpret_cast<DLGTEMPLATE*>(&m_buffer[0]);
      dlg->cdit++;

      DLGITEMTEMPLATE* item = reinterpret_cast<DLGITEMTEMPLATE*>(&m_buffer[ofs]);
      item->style = dwStyle;
      item->dwExtendedStyle = dwExtendedStyle;
      item->x = x;
      item->y = y;
      item->cx = cx;
      item->cy = cy;
      item->id = wId;

      WORD* meta = reinterpret_cast<WORD*>(&m_buffer[ofs + sizeof(DLGITEMTEMPLATE)]);
      meta[0] = 0xFFFF;
      meta[1] = wClassId;
      memcpy(meta + 2, lpszTitle, 2 * len);
    }

    void addStatic(WORD wId, short x, short y, short cx, short cy, DWORD dwStyle, LPCWSTR lpszTitle)
    {
      addControl(wId, STATIC, x, y, cx, cy, dwStyle | WS_GROUP | WS_VISIBLE, 0, lpszTitle);
    }

    void addButton(WORD wId, short x, short y, short cx, short cy, DWORD dwStyle, LPCWSTR lpszTitle)
    {
      addControl(wId, BUTTON, x, y, cx, cy, dwStyle | WS_TABSTOP | WS_VISIBLE, 0, lpszTitle);
    }

    void addEdit(WORD wId, short x, short y, short cx, short cy, DWORD dwStyle, LPCWSTR lpszTitle)
    {
      addControl(wId, EDIT, x, y, cx, cy, dwStyle | WS_BORDER | WS_TABSTOP | WS_VISIBLE, 0, lpszTitle);
    }

    operator DLGTEMPLATE*()
    {
      return reinterpret_cast<DLGTEMPLATE*>(&m_buffer[0]);
    }

  protected:
    std::vector<BYTE> m_buffer;
};

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  INITCOMMONCONTROLSEX icc;
  icc.dwSize = sizeof(icc);
  icc.dwICC = ICC_STANDARD_CLASSES;
  if (!InitCommonControlsEx(&icc))
    return -1;

  HWND hMainWnd;
  {
    DialogTemplate dlg(DS_CENTER | DS_SHELLFONT | WS_CAPTION | WS_SYSMENU, WS_EX_WINDOWEDGE, 0, 0, 304, 319, L"Proxy Test");
    dlg.setFont(8, L"MS Shell Dlg");
    dlg.addControl(-1, DialogTemplate::BUTTON, 7, 7, 290, 115,   BS_GROUPBOX | WS_VISIBLE, 0, L"Proxy settings for the current user");
    dlg.addButton (IDC_CONFIG_REFRESH,       238, 4,  50, 14,    BS_PUSHBUTTON, L"Refresh");
    dlg.addButton (IDC_CONFIG_AUTO,          17,  21, 270, 8,    BS_CHECKBOX, L"Automatically detect settings");
    dlg.addButton (IDC_CONFIG_SCRIPT,        17,  35, 270, 8,    BS_CHECKBOX, L"Use automatic configuration script:");
    dlg.addEdit   (IDC_CONFIG_SCRIPT_ADDR,   27,  45, 260, 13,   ES_AUTOHSCROLL | ES_READONLY, L"");
    dlg.addButton (IDC_CONFIG_CUSTOM,        17,  65, 270, 8,    BS_CHECKBOX, L"Use custom proxy server(s):");
    dlg.addEdit   (IDC_CONFIG_CUSTOM_ADDR,   27,  75, 260, 13,   ES_AUTOHSCROLL | ES_READONLY, L"");
    dlg.addStatic (-1,                       27,  90, 260, 8,    SS_LEFT, L"Exceptions:");
    dlg.addEdit   (IDC_CONFIG_CUSTOM_BYPASS, 27,  100, 260, 13,  ES_AUTOHSCROLL | ES_READONLY, L"");
    dlg.addStatic (-1,                       7,   132, 280, 8,   SS_LEFT, L"URLs to test (separated by whitespace):");
    dlg.addEdit   (IDC_INPUT,                7,   142, 225, 70,  WS_VSCROLL | ES_MULTILINE | ES_WANTRETURN,
      L"http://localhost/\r\n"
      L"http://127.0.0.1/\r\n"
      L"http://fud.cz/\r\n"
      L"http://pluto/something.mp3\r\n"
      L"http://foo.bar:secret.pass@long.domain.fud.cz/blah/blah/blah?meh=wtf@url&from:hell\r\n");
    dlg.addButton (IDC_RESOLVE,              237, 142, 50, 14,   BS_DEFPUSHBUTTON, L"Resolve");
    dlg.addStatic (-1,                       7,   216, 225, 8,   SS_LEFT, L"Output:");
    dlg.addEdit   (IDC_OUTPUT,               7,   227, 225, 85,  WS_VSCROLL | ES_MULTILINE | ES_READONLY, L"");
    dlg.addButton (IDC_OUTPUT_CLEAR,         238, 227, 50, 14,   BS_PUSHBUTTON, L"Clear");
    hMainWnd = CreateDialogIndirect(GetModuleHandle(NULL), dlg, NULL, &dialogProc);
  }
  if (!hMainWnd)
    return -1;

  ShowWindow(hMainWnd, nCmdShow);

  MSG msg;
  while (GetMessage(&msg, NULL, 0, 0) > 0) {
    if (IsDialogMessage(hMainWnd, &msg))
      continue;
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  return 0;
}
