[Openvpn-devel,v2,openvpn-gui] Update system tray to populate Windows VPN flyout

Message ID 20180725195112.27020-1-kkane@microsoft.com
State Deferred
Headers show
Series [Openvpn-devel,v2,openvpn-gui] Update system tray to populate Windows VPN flyout | expand

Commit Message

Kristof Provost via Openvpn-devel July 25, 2018, 9:51 a.m. UTC
Add a DLL to be wired in as a custom dialer, which introduces new build dependencies
Add copyright notices as required where Microsoft has contributed code

Signed-off-by: Kevin Kane <kkane@microsoft.com>
---
 .gitignore   |   3 +
 BUILD.rst    |   1 +
 Makefile.am  |  10 ++-
 configure.ac |   4 ++
 dialer.c     | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++
 tray.c       | 117 +++++++++++++++++++++++++++++++++++
 tray.h       |   2 +
 7 files changed, 307 insertions(+), 1 deletion(-)
 create mode 100644 dialer.c

Patch

diff --git a/.gitignore b/.gitignore
index ac37772..e36e0a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,8 @@ 
 *.log
 *.tar.gz
 *.bak
+*.lo
+*.la
 
 .deps
 Makefile
@@ -30,3 +32,4 @@  depcomp
 stamp-h1
 install-sh
 missing
+libtool
diff --git a/BUILD.rst b/BUILD.rst
index 9354784..3c70b52 100644
--- a/BUILD.rst
+++ b/BUILD.rst
@@ -24,6 +24,7 @@  their dependencies. You can install these packages using the standard
 - mingw64-x86_64-gcc-core
 - mingw64-x86_64-g++
 - mingw64-x86_64-openssl
+- libtool
 
 
 Build
diff --git a/Makefile.am b/Makefile.am
index 8301087..d8435ed 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,6 +2,7 @@ 
 #
 #  Copyright (C) 2004 Mathias Sundman <mathias@nilings.se>
 #                2010 Heiko Hund <heikoh@users.sf.net>
+#  Portions Copyright (C) 2018 Microsoft Corporation
 #
 #  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
@@ -22,6 +23,7 @@  RCCOMPILE = $(WINDRES) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \
     $(AM_CPPFLAGS) $(CPPFLAGS)
 
 AUTOMAKE_OPTIONS = foreign 1.9
+ACLOCAL_AMFLAGS = -I m4
 
 MAINTAINERCLEANFILES = \
 	config.log config.status \
@@ -106,7 +108,13 @@  openvpn_gui_LDADD = \
 	-lnetapi32 \
 	-lole32 \
 	-lshlwapi \
-	-lsecur32
+	-lsecur32 \
+	-lrasapi32
 
 openvpn-gui-res.o: $(openvpn_gui_RESOURCES) $(srcdir)/openvpn-gui-res.h
 	$(RCCOMPILE) -i $< -o $@
+
+lib_LTLIBRARIES              = libopenvpndialer.la
+libopenvpndialer_la_SOURCES  = dialer.c
+libopenvpndialer_la_LDFLAGS  = -no-undefined
+libopenvpndialer_la_LIBADD   = -lrasapi32
\ No newline at end of file
diff --git a/configure.ac b/configure.ac
index 9a2ba75..4fe6b81 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,6 +2,7 @@  dnl  OpenVPN-GUI -- A Windows GUI for OpenVPN.
 dnl
 dnl  Copyright (C) 2004 Mathias Sundman <mathias@nilings.se>
 dnl                2010 Heiko Hund <heikoh@users.sf.net>
+dnl  Portions Copyright (C) 2018 Microsoft Corporation
 dnl
 dnl  This program is free software; you can redistribute it and/or modify
 dnl  it under the terms of the GNU General Public License as published by
@@ -29,10 +30,13 @@  AC_DEFINE([CORE_COPYRIGHT_YEAR_END], ["2018"], [Last copyright year for daemon i
 AC_CONFIG_AUX_DIR([.])
 AM_CONFIG_HEADER([config.h])
 AC_CONFIG_SRCDIR([main.h])
+AC_CONFIG_MACRO_DIRS([m4])
 AM_INIT_AUTOMAKE
 AC_CANONICAL_HOST
 AC_USE_SYSTEM_EXTENSIONS
 AC_PROG_CC_C99
+LT_INIT([win32-dll])
+AC_LIBTOOL_WIN32_DLL
 AC_CHECK_TOOL([WINDRES], [windres])
 
 AC_ARG_ENABLE(
diff --git a/dialer.c b/dialer.c
new file mode 100644
index 0000000..86a0f39
--- /dev/null
+++ b/dialer.c
@@ -0,0 +1,171 @@ 
+/*
+ *  OpenVPN-GUI -- A Windows GUI for OpenVPN.
+ *
+ *  Copyright (C) 2018 Microsoft Corporation
+ *
+ *  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 (see the file COPYING included with this
+ *  distribution); if not, write to the Free Software Foundation, Inc.,
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <windows.h>
+#include <tchar.h>
+#include <ras.h>
+#include <rasdlg.h>
+#include <raserror.h>
+
+__declspec(dllexport)
+DWORD WINAPI RasCustomDial(
+    HINSTANCE hInstDll __attribute__((unused)),
+    LPRASDIALEXTENSIONS lpRasDialExtensions __attribute__((unused)),
+    LPWSTR lpszPhonebook __attribute__((unused)),
+    LPRASDIALPARAMSW lpRasDialParams __attribute__((unused)),
+    DWORD dwNotifierType __attribute__((unused)),
+    LPVOID lpvNotifier __attribute__((unused)),
+    LPHRASCONN lphRasConn __attribute__((unused)),
+    DWORD dwFlags __attribute__((unused)))
+{
+    return ERROR_CALL_NOT_IMPLEMENTED;
+}
+
+/* Copied from OpenVPN-GUI main.c */
+static const TCHAR OpenVPNGuiClassName[] = _T("OpenVPN-GUI");
+
+/* Copied from OpenVPN-GUI tray.h */
+#define IDM_CONNECTMENU         300
+
+static const TCHAR WindowCaption[] = _T("Walrus VPN");
+
+__declspec(dllexport)
+BOOL WINAPI RasCustomDialDlg(
+    HINSTANCE hInstDll __attribute__((unused)),
+    DWORD dwFlags __attribute__((unused)),
+    LPWSTR lpszwPhonebook,
+    LPWSTR lpszwEntry,
+    LPWSTR lpszwPhoneNumber __attribute__((unused)),
+    LPRASDIALDLG lpInfo,
+    PVOID pVoid __attribute__((unused)))
+{
+    HWND window = FindWindow(OpenVPNGuiClassName, NULL);
+    if (NULL == window)
+    {
+        MessageBox(NULL, _T("OpenVPN-GUI is not running. Please launch the GUI."), WindowCaption, MB_OK);
+        lpInfo->dwError = ERROR_UNKNOWN;
+        return FALSE;
+    }
+    else
+    {
+        DWORD result;
+        DWORD entrySize = 0;
+        LPRASENTRYW entry = NULL;
+
+        result = RasGetEntryPropertiesW(lpszwPhonebook, lpszwEntry, NULL, &entrySize, NULL, NULL);
+        if (ERROR_SUCCESS != result && ERROR_BUFFER_TOO_SMALL != result)
+        {
+            MessageBox(NULL, _T("Could not get size of VPN entry"), WindowCaption, MB_OK);
+            lpInfo->dwError = result;
+            return FALSE;
+        }
+
+        entry = (LPRASENTRYW)malloc(entrySize);
+        if (NULL == entry)
+        {
+            lpInfo->dwError = ERROR_OUTOFMEMORY;
+            return FALSE;
+        }
+
+        memset(entry, 0, entrySize);
+        entry->dwSize = sizeof(RASENTRYW);
+
+        result = RasGetEntryPropertiesW(lpszwPhonebook, lpszwEntry, entry, &entrySize, NULL, NULL);
+        if (ERROR_SUCCESS != result)
+        {
+            MessageBox(NULL, _T("Could not get VPN entry"), WindowCaption, MB_OK);
+            lpInfo->dwError = result;
+            free(entry);
+            return FALSE;
+        }
+
+        /* Convert the number in the local phone number field to get the offset. */
+        int offset = _wtoi(entry->szLocalPhoneNumber);
+
+        /* Sanity check: There should not be more than 10 entries, and the value should never be negative.*/
+        if (0 > offset || 10 < offset)
+        {
+            MessageBox(NULL, _T("Offset is invalid number"), WindowCaption, MB_OK);
+            lpInfo->dwError = ERROR_UNKNOWN;
+            free(entry);
+            return FALSE;
+        }
+
+        if (0 == offset)
+        {
+            /* 0 is also used for an error condition. If that's the case, check that the string is actually
+             * a zero.
+             */
+            if (entry->szLocalPhoneNumber[0] != L'0' && entry->szLocalPhoneNumber[1] != L'\0')
+            {
+                MessageBox(NULL, _T("Offset not recognized as a number"), WindowCaption, MB_OK);
+                lpInfo->dwError = ERROR_UNKNOWN;
+                free(entry);
+                return FALSE;
+            }
+        }
+
+        /* No longer need entry at this point. */
+        free(entry);
+
+        if (FALSE == PostMessage(window, WM_COMMAND, IDM_CONNECTMENU + offset, 0))
+        {
+            MessageBox(NULL, _T("Could not communicate with OpenVPN-GUI"), WindowCaption, MB_OK);
+            lpInfo->dwError = GetLastError();
+            return FALSE;
+        }
+        else
+        {
+            return TRUE;
+        }
+    }
+}
+
+__declspec(dllexport)
+DWORD WINAPI RasCustomHangUp(
+    HRASCONN hRasConn __attribute__((unused)))
+{
+    return ERROR_CALL_NOT_IMPLEMENTED;
+}
+
+__declspec(dllexport)
+BOOL WINAPI RasCustomEntryDlg(HINSTANCE hInstDll __attribute__((unused)),
+    LPWSTR lpszwPhonebook __attribute__((unused)),
+    LPWSTR lpszwEntry __attribute__((unused)),
+    LPRASENTRYDLG lpInfo,
+    DWORD dwFlags __attribute__((unused)))
+{
+    lpInfo->dwError = ERROR_CALL_NOT_IMPLEMENTED;
+    return FALSE;
+}
+
+__declspec(dllexport)
+DWORD CALLBACK RasCustomDeleteEntryNotify(
+    LPCTSTR lpszPhonebook __attribute__((unused)),
+    LPCTSTR lpszEntry __attribute__((unused)),
+    DWORD   dwFlags __attribute__((unused)))
+{
+    return ERROR_SUCCESS;
+}
diff --git a/tray.c b/tray.c
index 1e1dc2e..9ce27a0 100644
--- a/tray.c
+++ b/tray.c
@@ -3,6 +3,7 @@ 
  *
  *  Copyright (C) 2004 Mathias Sundman <mathias@nilings.se>
  *                2010 Heiko Hund <heikoh@users.sf.net>
+ *  Portions Copyright (C) 2018 Microsoft Corporation
  *
  *  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
@@ -28,6 +29,8 @@ 
 #include <shellapi.h>
 #include <tchar.h>
 #include <time.h>
+#include <ras.h>
+#include <raserror.h>
 
 #include "tray.h"
 #include "service.h"
@@ -47,6 +50,116 @@  HMENU hMenuService;
 NOTIFYICONDATA ni;
 extern options_t o;
 
+static const TCHAR VpnEntryPrefix[] = _T("OpenVPN ");
+static const TCHAR WindowCaption[] = _T("OpenVPN-GUI");
+
+/* Delete OpenVPN entries from Windows networking/VPN flyout */
+void
+ClearNetworkFlyout()
+{
+    DWORD result = ERROR_SUCCESS;
+    LPRASENTRYNAME entryName = NULL;
+    DWORD entryNameSize = 0;
+    DWORD entryCount = 0;
+
+    /* Determine memory required */
+    result = RasEnumEntries(NULL, NULL, NULL, &entryNameSize, &entryCount);
+    if (ERROR_BUFFER_TOO_SMALL != result)
+    {
+        /* Anything else is unexpected. */
+        MessageBox(NULL, _T("ClearNetworkFlyout: Could not get memory needed for RAS entries"), WindowCaption, MB_OK);
+        return;
+    }
+
+    entryName = (LPRASENTRYNAME)malloc(entryNameSize);
+    if (NULL == entryName)
+    {
+        /* No memory! */
+        MessageBox(NULL, _T("ClearNetworkFlyout: No memory allocating entry name"), WindowCaption, MB_OK);
+        return;
+    }
+
+    entryName[0].dwSize = sizeof(RASENTRYNAME);
+
+    result = RasEnumEntries(NULL, NULL, entryName, &entryNameSize, &entryCount);
+    if (ERROR_SUCCESS != result)
+    {
+        MessageBox(NULL, _T("ClearNetworkFlyout: Could not enumerate RAS entries"), WindowCaption, MB_OK);
+        free(entryName);
+        return;
+    }
+
+    for (DWORD i = 0; i < entryCount; i++)
+    {
+        if ((entryName[i].dwFlags == REN_User) &&
+            (0 == _tcsncmp(VpnEntryPrefix, entryName[i].szEntryName, _countof(VpnEntryPrefix) - 1)))
+        {
+            /* TODO: Retrieve the entry and verify that it references our custom dialer DLL? RasEnumEntries doesn't
+             * give us this information but we could retrieve the entry and check. This would keep us from purging any
+             * entries which for some inexplicable reason started with the VpnEntryPrefix string.
+             */
+            result = RasDeleteEntry(NULL, entryName[i].szEntryName);
+            if (ERROR_SUCCESS != result)
+            {
+                MessageBox(NULL, _T("ClearNetworkFlyout: Failed to delete entry"), WindowCaption, MB_OK);
+                /* Continue processing; don't return. */
+            }
+        }
+    }
+
+    free(entryName);
+
+    return;
+}
+
+static const TCHAR DialerFileName[] = _T("libopenvpndialer-0.dll");
+static const TCHAR DeviceName[] = _T("TAP-Windows Adapter V9");
+
+/* Create VPN flyout entry for a new connection */
+static void
+CreateNetworkFlyoutEntry(const connection_t *c, int number)
+{
+    DWORD result = ERROR_SUCCESS;
+    RASENTRY entry;
+    TCHAR entryName[RAS_MaxEntryName + 1];
+
+    memset(&entry, 0, sizeof(entry));
+    entry.dwSize = sizeof(entry);
+    _tcsncpy(entry.szDeviceType, RASDT_Vpn, _countof(RASDT_Vpn));
+    _tcsncpy(entry.szDeviceName, DeviceName, _countof(DeviceName));
+
+    entry.dwfOptions = RASEO_Custom;
+    /* Take the exe_path, strip off everything after the last backslash, and append DialerFileName */
+    TCHAR* lastBackslash = _tcsrchr(o.exe_path, _T('\\'));
+    if (NULL == lastBackslash)
+    {
+        MessageBox(NULL, _T("CreateNetworkFlyoutEntry: No last backslash found"), WindowCaption, MB_OK);
+        return;
+    }
+    _tcsncpy(entry.szCustomDialDll, o.exe_path, (lastBackslash - o.exe_path + 1));
+    /* Check for possible overrun */
+    if (_tcslen(entry.szCustomDialDll) + _countof(DialerFileName) >= _countof(entry.szCustomDialDll))
+    {
+        MessageBox(NULL, _T("CreateNetworkFlyoutEntry: Not enough space for custom dialer DLL filename"), WindowCaption, MB_OK);
+        return;
+    }
+    _tcsncat(entry.szCustomDialDll, DialerFileName, _countof(DialerFileName));
+
+    /* Use the local phone number space to store the entry number for the custom dialer to use. */
+    _sntprintf_0(entry.szLocalPhoneNumber, _T("%d"), number);
+    entry.dwType = RASET_Vpn;
+    entry.dwEncryptionType = ET_Optional; 
+
+    _sntprintf_0(entryName, _T("%s%s"), VpnEntryPrefix, c->config_name);
+    result = RasSetEntryProperties(NULL, entryName, &entry, sizeof(entry), NULL, 0);
+    if (ERROR_SUCCESS != result)
+    {
+        TCHAR message[512];
+        _sntprintf_0(message, _T("CreateNetworkFlyoutEntry: Could not RasSetEntryProperties: %lu"), result);
+        MessageBox(NULL, message, WindowCaption, MB_OK);
+        return;
+    }
+}
 
 /* Create popup menus */
 void
@@ -92,6 +205,7 @@  CreatePopupMenus()
         AppendMenu(hMenu, MF_STRING ,IDM_CLOSE, LoadLocalizedString(IDS_MENU_CLOSE));
 
         SetMenuStatus(&o.conn[0],  o.conn[0].state);
+        CreateNetworkFlyoutEntry(&o.conn[0], 0);
     }
     else {
         /* Create Main menu with all connections */
@@ -135,6 +249,7 @@  CreatePopupMenus()
 #endif
 
             SetMenuStatus(&o.conn[i], o.conn[i].state);
+            CreateNetworkFlyoutEntry(&o.conn[i], i);
         }
     }
 
@@ -152,6 +267,7 @@  DestroyPopupMenus()
 
     DestroyMenu(hMenuService);
     DestroyMenu(hMenu);
+    ClearNetworkFlyout();
 }
 
 
@@ -218,6 +334,7 @@  void
 OnDestroyTray()
 {
     DestroyMenu(hMenu);
+    ClearNetworkFlyout();
     Shell_NotifyIcon(NIM_DELETE, &ni);
 }
 
diff --git a/tray.h b/tray.h
index 1ce5e07..f5a9ff4 100644
--- a/tray.h
+++ b/tray.h
@@ -3,6 +3,7 @@ 
  *
  *  Copyright (C) 2004 Mathias Sundman <mathias@nilings.se>
  *                2010 Heiko Hund <heikoh@users.sf.net>
+ *  Portions Copyright (C) 2018 Microsoft Corporation
  *
  *  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
@@ -51,5 +52,6 @@  void SetMenuStatus(connection_t *, conn_state_t);
 void SetServiceMenuStatus();
 void ShowTrayBalloon(TCHAR *, TCHAR *);
 void CheckAndSetTrayIcon();
+void ClearNetworkFlyout();
 
 #endif