From 1079962e4c06f88a54e50d997c1b7e84303d30b4 Mon Sep 17 00:00:00 2001 From: Bernhard Schmidt Date: Sat, 15 Aug 2020 21:29:50 +0200 Subject: New upstream version 2.5~beta1 --- src/openvpnmsica/Makefile.am | 56 ++ src/openvpnmsica/dllmain.c | 198 ++++ src/openvpnmsica/msica_arg.c | 139 +++ src/openvpnmsica/msica_arg.h | 112 +++ src/openvpnmsica/msiex.c | 265 ++++++ src/openvpnmsica/msiex.h | 112 +++ src/openvpnmsica/openvpnmsica-Debug.props | 14 + src/openvpnmsica/openvpnmsica-Release.props | 14 + src/openvpnmsica/openvpnmsica.c | 1215 +++++++++++++++++++++++++ src/openvpnmsica/openvpnmsica.h | 153 ++++ src/openvpnmsica/openvpnmsica.props | 18 + src/openvpnmsica/openvpnmsica.vcxproj | 142 +++ src/openvpnmsica/openvpnmsica.vcxproj.filters | 62 ++ src/openvpnmsica/openvpnmsica_resources.rc | 62 ++ 14 files changed, 2562 insertions(+) create mode 100644 src/openvpnmsica/Makefile.am create mode 100644 src/openvpnmsica/dllmain.c create mode 100644 src/openvpnmsica/msica_arg.c create mode 100644 src/openvpnmsica/msica_arg.h create mode 100644 src/openvpnmsica/msiex.c create mode 100644 src/openvpnmsica/msiex.h create mode 100644 src/openvpnmsica/openvpnmsica-Debug.props create mode 100644 src/openvpnmsica/openvpnmsica-Release.props create mode 100644 src/openvpnmsica/openvpnmsica.c create mode 100644 src/openvpnmsica/openvpnmsica.h create mode 100644 src/openvpnmsica/openvpnmsica.props create mode 100644 src/openvpnmsica/openvpnmsica.vcxproj create mode 100644 src/openvpnmsica/openvpnmsica.vcxproj.filters create mode 100644 src/openvpnmsica/openvpnmsica_resources.rc (limited to 'src/openvpnmsica') diff --git a/src/openvpnmsica/Makefile.am b/src/openvpnmsica/Makefile.am new file mode 100644 index 0000000..9d18854 --- /dev/null +++ b/src/openvpnmsica/Makefile.am @@ -0,0 +1,56 @@ +# +# openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages +# +# Copyright (C) 2002-2018 OpenVPN Inc +# Copyright (C) 2018-2020 Simon Rozman +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 +# as published by the Free Software Foundation. +# +# 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. +# + +include $(top_srcdir)/build/ltrc.inc + +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in + +EXTRA_DIST = \ + openvpnmsica.vcxproj \ + openvpnmsica.vcxproj.filters \ + openvpnmsica.props \ + openvpnmsica-Debug.props \ + openvpnmsica-Release.props + +AM_CPPFLAGS = \ + -I$(top_srcdir)/include -I$(top_srcdir)/src/compat + +AM_CFLAGS = \ + $(TAP_CFLAGS) + +if WIN32 +lib_LTLIBRARIES = libopenvpnmsica.la +libopenvpnmsica_la_CFLAGS = \ + -municode -D_UNICODE \ + -UNTDDI_VERSION -U_WIN32_WINNT \ + -D_WIN32_WINNT=_WIN32_WINNT_VISTA \ + -Wl,--kill-at +libopenvpnmsica_la_LDFLAGS = -ladvapi32 -lole32 -lmsi -lsetupapi -liphlpapi -lshell32 -lshlwapi -lversion -no-undefined -avoid-version +endif + +libopenvpnmsica_la_SOURCES = \ + dllmain.c \ + msiex.c msiex.h \ + msica_arg.c msica_arg.h \ + openvpnmsica.c openvpnmsica.h \ + $(top_srcdir)/src/tapctl/basic.h \ + $(top_srcdir)/src/tapctl/error.c $(top_srcdir)/src/tapctl/error.h \ + $(top_srcdir)/src/tapctl/tap.c $(top_srcdir)/src/tapctl/tap.h \ + openvpnmsica_resources.rc diff --git a/src/openvpnmsica/dllmain.c b/src/openvpnmsica/dllmain.c new file mode 100644 index 0000000..201fd9a --- /dev/null +++ b/src/openvpnmsica/dllmain.c @@ -0,0 +1,198 @@ +/* + * openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages + * https://community.openvpn.net/openvpn/wiki/OpenVPNMSICA + * + * Copyright (C) 2018 Simon Rozman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include +#elif defined(_MSC_VER) +#include +#endif + +#include "openvpnmsica.h" +#include "../tapctl/error.h" + +#include +#include +#include +#ifdef _MSC_VER +#pragma comment(lib, "msi.lib") +#endif +#include +#include + + +DWORD openvpnmsica_thread_data_idx = TLS_OUT_OF_INDEXES; + + +/** + * DLL entry point + */ +BOOL WINAPI +DllMain( + _In_ HINSTANCE hinstDLL, + _In_ DWORD dwReason, + _In_ LPVOID lpReserved) +{ + UNREFERENCED_PARAMETER(hinstDLL); + UNREFERENCED_PARAMETER(lpReserved); + + switch (dwReason) + { + case DLL_PROCESS_ATTACH: + /* Allocate thread local storage index. */ + openvpnmsica_thread_data_idx = TlsAlloc(); + if (openvpnmsica_thread_data_idx == TLS_OUT_OF_INDEXES) + { + return FALSE; + } + /* Fall through. */ + + case DLL_THREAD_ATTACH: + { + /* Create thread local storage data. */ + struct openvpnmsica_thread_data *s = (struct openvpnmsica_thread_data *)calloc(1, sizeof(struct openvpnmsica_thread_data)); + if (s == NULL) + { + return FALSE; + } + + TlsSetValue(openvpnmsica_thread_data_idx, s); + break; + } + + case DLL_PROCESS_DETACH: + if (openvpnmsica_thread_data_idx != TLS_OUT_OF_INDEXES) + { + /* Free thread local storage data and index. */ + free(TlsGetValue(openvpnmsica_thread_data_idx)); + TlsFree(openvpnmsica_thread_data_idx); + } + break; + + case DLL_THREAD_DETACH: + /* Free thread local storage data. */ + free(TlsGetValue(openvpnmsica_thread_data_idx)); + break; + } + + return TRUE; +} + + +bool +dont_mute(unsigned int flags) +{ + UNREFERENCED_PARAMETER(flags); + + return true; +} + + +void +x_msg_va(const unsigned int flags, const char *format, va_list arglist) +{ + /* Secure last error before it is overridden. */ + DWORD dwResult = (flags & M_ERRNO) != 0 ? GetLastError() : ERROR_SUCCESS; + + struct openvpnmsica_thread_data *s = (struct openvpnmsica_thread_data *)TlsGetValue(openvpnmsica_thread_data_idx); + if (s->hInstall == 0) + { + /* No MSI session, no fun. */ + return; + } + + /* Prepare the message record. The record will contain up to four fields. */ + MSIHANDLE hRecordProg = MsiCreateRecord(4); + + { + /* Field 2: The message string. */ + char szBufStack[128]; + int iResultLen = vsnprintf(szBufStack, _countof(szBufStack), format, arglist); + if (iResultLen < _countof(szBufStack)) + { + /* Use from stack. */ + MsiRecordSetStringA(hRecordProg, 2, szBufStack); + } + else + { + /* Allocate on heap and retry. */ + char *szMessage = (char *)malloc(++iResultLen * sizeof(char)); + if (szMessage != NULL) + { + vsnprintf(szMessage, iResultLen, format, arglist); + MsiRecordSetStringA(hRecordProg, 2, szMessage); + free(szMessage); + } + else + { + /* Use stack variant anyway, but make sure it's zero-terminated. */ + szBufStack[_countof(szBufStack) - 1] = 0; + MsiRecordSetStringA(hRecordProg, 2, szBufStack); + } + } + } + + if ((flags & M_ERRNO) == 0) + { + /* Field 1: MSI Error Code */ + MsiRecordSetInteger(hRecordProg, 1, ERROR_MSICA); + } + else + { + /* Field 1: MSI Error Code */ + MsiRecordSetInteger(hRecordProg, 1, ERROR_MSICA_ERRNO); + + /* Field 3: The Windows error number. */ + MsiRecordSetInteger(hRecordProg, 3, dwResult); + + /* Field 4: The Windows error description. */ + LPTSTR szErrMessage = NULL; + if (FormatMessage( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS, + 0, + dwResult, + 0, + (LPTSTR)&szErrMessage, + 0, + NULL) && szErrMessage) + { + /* Trim trailing whitespace. Set terminator after the last non-whitespace character. This prevents excessive trailing line breaks. */ + for (size_t i = 0, i_last = 0;; i++) + { + if (szErrMessage[i]) + { + if (!_istspace(szErrMessage[i])) + { + i_last = i + 1; + } + } + else + { + szErrMessage[i_last] = 0; + break; + } + } + MsiRecordSetString(hRecordProg, 4, szErrMessage); + LocalFree(szErrMessage); + } + } + + MsiProcessMessage(s->hInstall, INSTALLMESSAGE_ERROR, hRecordProg); + MsiCloseHandle(hRecordProg); +} diff --git a/src/openvpnmsica/msica_arg.c b/src/openvpnmsica/msica_arg.c new file mode 100644 index 0000000..0014537 --- /dev/null +++ b/src/openvpnmsica/msica_arg.c @@ -0,0 +1,139 @@ +/* + * openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages + * https://community.openvpn.net/openvpn/wiki/OpenVPNMSICA + * + * Copyright (C) 2018-2020 Simon Rozman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include +#elif defined(_MSC_VER) +#include +#endif + +#include "msica_arg.h" +#include "../tapctl/error.h" +#include "../tapctl/tap.h" + +#include +#include + + +void +msica_arg_seq_init(_Inout_ struct msica_arg_seq *seq) +{ + seq->head = NULL; + seq->tail = NULL; +} + + +void +msica_arg_seq_free(_Inout_ struct msica_arg_seq *seq) +{ + while (seq->head) + { + struct msica_arg *p = seq->head; + seq->head = seq->head->next; + free(p); + } + seq->tail = NULL; +} + + +void +msica_arg_seq_add_head( + _Inout_ struct msica_arg_seq *seq, + _In_z_ LPCTSTR argument) +{ + size_t argument_size = (_tcslen(argument) + 1) * sizeof(TCHAR); + struct msica_arg *p = malloc(sizeof(struct msica_arg) + argument_size); + if (p == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, sizeof(struct msica_arg) + argument_size); + } + memcpy(p->val, argument, argument_size); + p->next = seq->head; + seq->head = p; + if (seq->tail == NULL) + { + seq->tail = p; + } +} + + +void +msica_arg_seq_add_tail( + _Inout_ struct msica_arg_seq *seq, + _Inout_ LPCTSTR argument) +{ + size_t argument_size = (_tcslen(argument) + 1) * sizeof(TCHAR); + struct msica_arg *p = malloc(sizeof(struct msica_arg) + argument_size); + if (p == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, sizeof(struct msica_arg) + argument_size); + } + memcpy(p->val, argument, argument_size); + p->next = NULL; + *(seq->tail ? &seq->tail->next : &seq->head) = p; + seq->tail = p; +} + + +LPTSTR +msica_arg_seq_join(_In_ const struct msica_arg_seq *seq) +{ + /* Count required space. */ + size_t size = 2 /*x + zero-terminator*/; + for (struct msica_arg *p = seq->head; p != NULL; p = p->next) + { + size += _tcslen(p->val) + 1 /*space delimiter|zero-terminator*/; + } + size *= sizeof(TCHAR); + + /* Allocate. */ + LPTSTR str = malloc(size); + if (str == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, size); + return NULL; + } + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4996) /* Using unsafe string functions: The space in s and termination of p->val has been implicitly verified at the beginning of this function. */ +#endif + + /* Dummy argv[0] (i.e. executable name), for CommandLineToArgvW to work correctly when parsing this string. */ + _tcscpy(str, TEXT("x")); + + /* Join. */ + LPTSTR s = str + 1 /*x*/; + for (struct msica_arg *p = seq->head; p != NULL; p = p->next) + { + /* Convert zero-terminator into space delimiter. */ + s[0] = TEXT(' '); + s++; + /* Append argument. */ + _tcscpy(s, p->val); + s += _tcslen(p->val); + } + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + + return str; +} diff --git a/src/openvpnmsica/msica_arg.h b/src/openvpnmsica/msica_arg.h new file mode 100644 index 0000000..d2158e0 --- /dev/null +++ b/src/openvpnmsica/msica_arg.h @@ -0,0 +1,112 @@ +/* + * openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages + * https://community.openvpn.net/openvpn/wiki/OpenVPNMSICA + * + * Copyright (C) 2018-2020 Simon Rozman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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. + */ + +#ifndef MSICA_ARG_H +#define MSICA_ARG_H + +#include +#include +#include "../tapctl/basic.h" + + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4200) /* Using zero-sized arrays in struct/union. */ +#endif + + +/** + * Argument list + */ +struct msica_arg +{ + struct msica_arg *next; /** Pointer to the next argument in the sequence */ + TCHAR val[]; /** Zero terminated argument string */ +}; + + +/** + * Argument sequence + */ +struct msica_arg_seq +{ + struct msica_arg *head; /** Pointer to the first argument in the sequence */ + struct msica_arg *tail; /** Pointer to the last argument in the sequence */ +}; + + +/** + * Initializes argument sequence + * + * @param seq Pointer to uninitialized argument sequence + */ +void +msica_arg_seq_init(_Inout_ struct msica_arg_seq *seq); + + +/** + * Frees argument sequence + * + * @param seq Pointer to the argument sequence + */ +void +msica_arg_seq_free(_Inout_ struct msica_arg_seq *seq); + + +/** + * Inserts argument to the beginning of the argument sequence + * + * @param seq Pointer to the argument sequence + * + * @param argument Zero-terminated argument string to insert. + */ +void +msica_arg_seq_add_head( + _Inout_ struct msica_arg_seq *seq, + _In_z_ LPCTSTR argument); + + +/** + * Appends argument to the end of the argument sequence + * + * @param seq Pointer to the argument sequence + * + * @param argument Zero-terminated argument string to append. + */ +void +msica_arg_seq_add_tail( + _Inout_ struct msica_arg_seq *seq, + _Inout_ LPCTSTR argument); + +/** + * Join arguments of the argument sequence into a space delimited string + * + * @param seq Pointer to the argument sequence + * + * @return Joined argument string. Must be released with free() after use. + */ +LPTSTR +msica_arg_seq_join(_In_ const struct msica_arg_seq *seq); + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#endif /* ifndef MSICA_ARG_H */ diff --git a/src/openvpnmsica/msiex.c b/src/openvpnmsica/msiex.c new file mode 100644 index 0000000..00265d0 --- /dev/null +++ b/src/openvpnmsica/msiex.c @@ -0,0 +1,265 @@ +/* + * openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages + * https://community.openvpn.net/openvpn/wiki/OpenVPNMSICA + * + * Copyright (C) 2018 Simon Rozman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include +#elif defined(_MSC_VER) +#include +#endif + +#include "msiex.h" +#include "../tapctl/error.h" + +#include +#include +#include +#include +#ifdef _MSC_VER +#pragma comment(lib, "msi.lib") +#endif + + +UINT +msi_get_string( + _In_ MSIHANDLE hInstall, + _In_z_ LPCTSTR szName, + _Out_ LPTSTR *pszValue) +{ + if (pszValue == NULL) + { + return ERROR_BAD_ARGUMENTS; + } + + /* Try with stack buffer first. */ + TCHAR szBufStack[128]; + DWORD dwLength = _countof(szBufStack); + UINT uiResult = MsiGetProperty(hInstall, szName, szBufStack, &dwLength); + if (uiResult == ERROR_SUCCESS) + { + /* Copy from stack. */ + *pszValue = (LPTSTR)malloc(++dwLength * sizeof(TCHAR)); + if (*pszValue == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", dwLength * sizeof(TCHAR)); + return ERROR_OUTOFMEMORY; + } + + memcpy(*pszValue, szBufStack, dwLength * sizeof(TCHAR)); + return ERROR_SUCCESS; + } + else if (uiResult == ERROR_MORE_DATA) + { + /* Allocate on heap and retry. */ + LPTSTR szBufHeap = (LPTSTR)malloc(++dwLength * sizeof(TCHAR)); + if (szBufHeap == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwLength * sizeof(TCHAR)); + return ERROR_OUTOFMEMORY; + } + + uiResult = MsiGetProperty(hInstall, szName, szBufHeap, &dwLength); + if (uiResult == ERROR_SUCCESS) + { + *pszValue = szBufHeap; + } + else + { + free(szBufHeap); + } + return uiResult; + } + else + { + SetLastError(uiResult); /* MSDN does not mention MsiGetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiGetProperty failed", __FUNCTION__); + return uiResult; + } +} + + +UINT +msi_get_record_string( + _In_ MSIHANDLE hRecord, + _In_ unsigned int iField, + _Out_ LPTSTR *pszValue) +{ + if (pszValue == NULL) + { + return ERROR_BAD_ARGUMENTS; + } + + /* Try with stack buffer first. */ + TCHAR szBufStack[128]; + DWORD dwLength = _countof(szBufStack); + UINT uiResult = MsiRecordGetString(hRecord, iField, szBufStack, &dwLength); + if (uiResult == ERROR_SUCCESS) + { + /* Copy from stack. */ + *pszValue = (LPTSTR)malloc(++dwLength * sizeof(TCHAR)); + if (*pszValue == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwLength * sizeof(TCHAR)); + return ERROR_OUTOFMEMORY; + } + + memcpy(*pszValue, szBufStack, dwLength * sizeof(TCHAR)); + return ERROR_SUCCESS; + } + else if (uiResult == ERROR_MORE_DATA) + { + /* Allocate on heap and retry. */ + LPTSTR szBufHeap = (LPTSTR)malloc(++dwLength * sizeof(TCHAR)); + if (szBufHeap == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwLength * sizeof(TCHAR)); + return ERROR_OUTOFMEMORY; + } + + uiResult = MsiRecordGetString(hRecord, iField, szBufHeap, &dwLength); + if (uiResult == ERROR_SUCCESS) + { + *pszValue = szBufHeap; + } + else + { + free(szBufHeap); + } + return uiResult; + } + else + { + SetLastError(uiResult); /* MSDN does not mention MsiRecordGetString() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiRecordGetString failed", __FUNCTION__); + return uiResult; + } +} + + +UINT +msi_format_record( + _In_ MSIHANDLE hInstall, + _In_ MSIHANDLE hRecord, + _Out_ LPTSTR *pszValue) +{ + if (pszValue == NULL) + { + return ERROR_BAD_ARGUMENTS; + } + + /* Try with stack buffer first. */ + TCHAR szBufStack[128]; + DWORD dwLength = _countof(szBufStack); + UINT uiResult = MsiFormatRecord(hInstall, hRecord, szBufStack, &dwLength); + if (uiResult == ERROR_SUCCESS) + { + /* Copy from stack. */ + *pszValue = (LPTSTR)malloc(++dwLength * sizeof(TCHAR)); + if (*pszValue == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwLength * sizeof(TCHAR)); + return ERROR_OUTOFMEMORY; + } + + memcpy(*pszValue, szBufStack, dwLength * sizeof(TCHAR)); + return ERROR_SUCCESS; + } + else if (uiResult == ERROR_MORE_DATA) + { + /* Allocate on heap and retry. */ + LPTSTR szBufHeap = (LPTSTR)malloc(++dwLength * sizeof(TCHAR)); + if (szBufHeap == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwLength * sizeof(TCHAR)); + return ERROR_OUTOFMEMORY; + } + + uiResult = MsiFormatRecord(hInstall, hRecord, szBufHeap, &dwLength); + if (uiResult == ERROR_SUCCESS) + { + *pszValue = szBufHeap; + } + else + { + free(szBufHeap); + } + return uiResult; + } + else + { + SetLastError(uiResult); /* MSDN does not mention MsiFormatRecord() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiFormatRecord failed", __FUNCTION__); + return uiResult; + } +} + + +UINT +msi_format_field( + _In_ MSIHANDLE hInstall, + _In_ MSIHANDLE hRecord, + _In_ unsigned int iField, + _Out_ LPTSTR *pszValue) +{ + if (pszValue == NULL) + { + return ERROR_BAD_ARGUMENTS; + } + + /* Read string to format. */ + LPTSTR szValue = NULL; + UINT uiResult = msi_get_record_string(hRecord, iField, &szValue); + if (uiResult != ERROR_SUCCESS) + { + return uiResult; + } + if (szValue[0] == 0) + { + /* The string is empty. There's nothing left to do. */ + *pszValue = szValue; + return ERROR_SUCCESS; + } + + /* Create a temporary record. */ + MSIHANDLE hRecordEx = MsiCreateRecord(1); + if (!hRecordEx) + { + uiResult = ERROR_INVALID_HANDLE; + msg(M_NONFATAL, "%s: MsiCreateRecord failed", __FUNCTION__); + goto cleanup_szValue; + } + + /* Populate the record with data. */ + uiResult = MsiRecordSetString(hRecordEx, 0, szValue); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiRecordSetString() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiRecordSetString failed", __FUNCTION__); + goto cleanup_hRecordEx; + } + + /* Do the formatting. */ + uiResult = msi_format_record(hInstall, hRecordEx, pszValue); + +cleanup_hRecordEx: + MsiCloseHandle(hRecordEx); +cleanup_szValue: + free(szValue); + return uiResult; +} diff --git a/src/openvpnmsica/msiex.h b/src/openvpnmsica/msiex.h new file mode 100644 index 0000000..d819b87 --- /dev/null +++ b/src/openvpnmsica/msiex.h @@ -0,0 +1,112 @@ +/* + * openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages + * https://community.openvpn.net/openvpn/wiki/OpenVPNMSICA + * + * Copyright (C) 2018 Simon Rozman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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. + */ + +#ifndef MSIHLP_H +#define MSIHLP_H + +#include +#include +#include "../tapctl/basic.h" + + +/** + * Gets MSI property value + * + * @param hInstall Handle to the installation provided to the DLL custom action + * + * @param szName Property name + * + * @param pszValue Pointer to string to retrieve property value. The string must + * be released with free() after use. + * + * @return ERROR_SUCCESS on success; Win32 error code otherwise + */ +UINT +msi_get_string( + _In_ MSIHANDLE hInstall, + _In_z_ LPCTSTR szName, + _Out_ LPTSTR *pszValue); + + +/** + * Gets MSI record string value + * + * @param hRecord Handle to the record + * + * @param iField Field index + * + * @param pszValue Pointer to string to retrieve field value. The string must be + * released with free() after use. + * + * @return ERROR_SUCCESS on success; Win32 error code otherwise + */ +UINT +msi_get_record_string( + _In_ MSIHANDLE hRecord, + _In_ unsigned int iField, + _Out_ LPTSTR *pszValue); + + +/** + * Formats MSI record + * + * @param hInstall Handle to the installation. This may be omitted, in which case only the + * record field parameters are processed and properties are not available + * for substitution. + * + * @param hRecord Handle to the record to format. The template string must be stored in + * record field 0 followed by referenced data parameters. + * + * @param pszValue Pointer to string to retrieve formatted value. The string must be + * released with free() after use. + * + * @return ERROR_SUCCESS on success; Win32 error code otherwise + */ +UINT +msi_format_record( + _In_ MSIHANDLE hInstall, + _In_ MSIHANDLE hRecord, + _Out_ LPTSTR *pszValue); + + +/** + * Formats MSI record field + * + * @param hInstall Handle to the installation. This may be omitted, in which case only the + * record field parameters are processed and properties are not available + * for substitution. + * + * @param hRecord Handle to the field record + * + * @param iField Field index + * + * @param pszValue Pointer to string to retrieve formatted value. The string must be + * released with free() after use. + * + * @return ERROR_SUCCESS on success; Win32 error code otherwise + */ +UINT +msi_format_field( + _In_ MSIHANDLE hInstall, + _In_ MSIHANDLE hRecord, + _In_ unsigned int iField, + _Out_ LPTSTR *pszValue); + +#endif /* ifndef MSIHLP_H */ diff --git a/src/openvpnmsica/openvpnmsica-Debug.props b/src/openvpnmsica/openvpnmsica-Debug.props new file mode 100644 index 0000000..43532cf --- /dev/null +++ b/src/openvpnmsica/openvpnmsica-Debug.props @@ -0,0 +1,14 @@ + + + + + + + + + + MultiThreadedDebug + + + + \ No newline at end of file diff --git a/src/openvpnmsica/openvpnmsica-Release.props b/src/openvpnmsica/openvpnmsica-Release.props new file mode 100644 index 0000000..848fda8 --- /dev/null +++ b/src/openvpnmsica/openvpnmsica-Release.props @@ -0,0 +1,14 @@ + + + + + + + + + + MultiThreaded + + + + \ No newline at end of file diff --git a/src/openvpnmsica/openvpnmsica.c b/src/openvpnmsica/openvpnmsica.c new file mode 100644 index 0000000..31e90bd --- /dev/null +++ b/src/openvpnmsica/openvpnmsica.c @@ -0,0 +1,1215 @@ +/* + * openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages + * https://community.openvpn.net/openvpn/wiki/OpenVPNMSICA + * + * Copyright (C) 2018-2020 Simon Rozman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include +#elif defined(_MSC_VER) +#include +#endif +#include /* Must be included _before_ */ + +#include "openvpnmsica.h" +#include "msica_arg.h" +#include "msiex.h" + +#include "../tapctl/basic.h" +#include "../tapctl/error.h" +#include "../tapctl/tap.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "iphlpapi.lib") +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "version.lib") +#endif + + +/** + * Local constants + */ + +#define MSICA_ADAPTER_TICK_SIZE (16*1024) /** Amount of tick space to reserve for one TAP/TUN adapter creation/deletition. */ + + +/** + * Joins an argument sequence and sets it to the MSI property. + * + * @param hInstall Handle to the installation provided to the DLL custom action + * + * @param szProperty MSI property name to set to the joined argument sequence. + * + * @param seq The argument sequence. + * + * @return ERROR_SUCCESS on success; An error code otherwise + */ +static UINT +setup_sequence( + _In_ MSIHANDLE hInstall, + _In_z_ LPCTSTR szProperty, + _In_ struct msica_arg_seq *seq) +{ + UINT uiResult; + LPTSTR szSequence = msica_arg_seq_join(seq); + uiResult = MsiSetProperty(hInstall, szProperty, szSequence); + free(szSequence); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szProperty); + return uiResult; + } + return ERROR_SUCCESS; +} + + +#ifdef _DEBUG + +/** + * Pops up a message box creating a time window to attach a debugger to the installer process in + * order to debug custom actions. + * + * @param szFunctionName Function name that triggered the pop-up. Displayed in message box's + * title. + */ +static void +_debug_popup(_In_z_ LPCTSTR szFunctionName) +{ + TCHAR szTitle[0x100], szMessage[0x100+MAX_PATH], szProcessPath[MAX_PATH]; + + /* Compose pop-up title. The dialog title will contain function name to ease the process + * locating. Mind that Visual Studio displays window titles on the process list. */ + _stprintf_s(szTitle, _countof(szTitle), TEXT("%s v%s"), szFunctionName, TEXT(PACKAGE_VERSION)); + + /* Get process name. */ + GetModuleFileName(NULL, szProcessPath, _countof(szProcessPath)); + LPCTSTR szProcessName = _tcsrchr(szProcessPath, TEXT('\\')); + szProcessName = szProcessName ? szProcessName + 1 : szProcessPath; + + /* Compose the pop-up message. */ + _stprintf_s( + szMessage, _countof(szMessage), + TEXT("The %s process (PID: %u) has started to execute the %s custom action.\r\n") + TEXT("\r\n") + TEXT("If you would like to debug the custom action, attach a debugger to this process and set breakpoints before dismissing this dialog.\r\n") + TEXT("\r\n") + TEXT("If you are not debugging this custom action, you can safely ignore this message."), + szProcessName, + GetCurrentProcessId(), + szFunctionName); + + MessageBox(NULL, szMessage, szTitle, MB_OK); +} + +#define debug_popup(f) _debug_popup(f) +#else /* ifdef _DEBUG */ +#define debug_popup(f) +#endif /* ifdef _DEBUG */ + + +/** + * Detects if the OpenVPNService service is in use (running or paused) and sets + * OPENVPNSERVICE to the service process PID, or its path if it is set to + * auto-start, but not running. + * + * @param hInstall Handle to the installation provided to the DLL custom action + * + * @return ERROR_SUCCESS on success; An error code otherwise + * See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072.aspx + */ +static UINT +set_openvpnserv_state(_In_ MSIHANDLE hInstall) +{ + UINT uiResult; + + /* Get Service Control Manager handle. */ + SC_HANDLE hSCManager = OpenSCManager(NULL, SERVICES_ACTIVE_DATABASE, SC_MANAGER_CONNECT); + if (hSCManager == NULL) + { + uiResult = GetLastError(); + msg(M_NONFATAL | M_ERRNO, "%s: OpenSCManager() failed", __FUNCTION__); + return uiResult; + } + + /* Get OpenVPNService service handle. */ + SC_HANDLE hService = OpenService(hSCManager, TEXT("OpenVPNService"), SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); + if (hService == NULL) + { + uiResult = GetLastError(); + if (uiResult == ERROR_SERVICE_DOES_NOT_EXIST) + { + /* This is not actually an error. */ + goto cleanup_OpenSCManager; + } + msg(M_NONFATAL | M_ERRNO, "%s: OpenService(\"OpenVPNService\") failed", __FUNCTION__); + goto cleanup_OpenSCManager; + } + + /* Query service status. */ + SERVICE_STATUS_PROCESS ssp; + DWORD dwBufSize; + if (QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp, sizeof(ssp), &dwBufSize)) + { + switch (ssp.dwCurrentState) + { + case SERVICE_START_PENDING: + case SERVICE_RUNNING: + case SERVICE_STOP_PENDING: + case SERVICE_PAUSE_PENDING: + case SERVICE_PAUSED: + case SERVICE_CONTINUE_PENDING: + { + /* Service is started (kind of). Set OPENVPNSERVICE property to service PID. */ + TCHAR szPID[10 /*MAXDWORD in decimal*/ + 1 /*terminator*/]; + _stprintf_s( + szPID, _countof(szPID), + TEXT("%u"), + ssp.dwProcessId); + + uiResult = MsiSetProperty(hInstall, TEXT("OPENVPNSERVICE"), szPID); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"OPENVPNSERVICE\") failed", __FUNCTION__); + } + + /* We know user is using the service. Skip auto-start setting check. */ + goto cleanup_OpenService; + } + break; + } + } + else + { + uiResult = GetLastError(); + msg(M_NONFATAL | M_ERRNO, "%s: QueryServiceStatusEx(\"OpenVPNService\") failed", __FUNCTION__); + } + + /* Service is not started. Is it set to auto-start? */ + /* MSDN describes the maximum buffer size for QueryServiceConfig() to be 8kB. */ + /* This is small enough to fit on stack. */ + BYTE _buffer_8k[8192]; + LPQUERY_SERVICE_CONFIG pQsc = (LPQUERY_SERVICE_CONFIG)_buffer_8k; + dwBufSize = sizeof(_buffer_8k); + if (!QueryServiceConfig(hService, pQsc, dwBufSize, &dwBufSize)) + { + uiResult = GetLastError(); + msg(M_NONFATAL | M_ERRNO, "%s: QueryServiceStatusEx(\"QueryServiceConfig\") failed", __FUNCTION__); + goto cleanup_OpenService; + } + + if (pQsc->dwStartType <= SERVICE_AUTO_START) + { + /* Service is set to auto-start. Set OPENVPNSERVICE property to its path. */ + uiResult = MsiSetProperty(hInstall, TEXT("OPENVPNSERVICE"), pQsc->lpBinaryPathName); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"OPENVPNSERVICE\") failed", __FUNCTION__); + goto cleanup_OpenService; + } + } + + uiResult = ERROR_SUCCESS; + +cleanup_OpenService: + CloseServiceHandle(hService); +cleanup_OpenSCManager: + CloseServiceHandle(hSCManager); + return uiResult; +} + + +static UINT +find_adapters( + _In_ MSIHANDLE hInstall, + _In_z_ LPCTSTR szzHardwareIDs, + _In_z_ LPCTSTR szAdaptersPropertyName, + _In_z_ LPCTSTR szActiveAdaptersPropertyName) +{ + UINT uiResult; + + /* Get network adapters with given hardware ID. */ + struct tap_adapter_node *pAdapterList = NULL; + uiResult = tap_list_adapters(NULL, szzHardwareIDs, &pAdapterList); + if (uiResult != ERROR_SUCCESS) + { + return uiResult; + } + else if (pAdapterList == NULL) + { + /* No adapters - no fun. */ + return ERROR_SUCCESS; + } + + /* Get IPv4/v6 info for all network adapters. Actually, we're interested in link status only: up/down? */ + PIP_ADAPTER_ADDRESSES pAdapterAdresses = NULL; + ULONG ulAdapterAdressesSize = 16*1024; + for (size_t iteration = 0; iteration < 2; iteration++) + { + pAdapterAdresses = (PIP_ADAPTER_ADDRESSES)malloc(ulAdapterAdressesSize); + if (pAdapterAdresses == NULL) + { + msg(M_NONFATAL, "%s: malloc(%u) failed", __FUNCTION__, ulAdapterAdressesSize); + uiResult = ERROR_OUTOFMEMORY; goto cleanup_pAdapterList; + } + + ULONG ulResult = GetAdaptersAddresses( + AF_UNSPEC, + GAA_FLAG_SKIP_UNICAST | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_INCLUDE_ALL_INTERFACES, + NULL, + pAdapterAdresses, + &ulAdapterAdressesSize); + + if (ulResult == ERROR_SUCCESS) + { + break; + } + + free(pAdapterAdresses); + if (ulResult != ERROR_BUFFER_OVERFLOW) + { + SetLastError(ulResult); /* MSDN does not mention GetAdaptersAddresses() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: GetAdaptersAddresses() failed", __FUNCTION__); + uiResult = ulResult; goto cleanup_pAdapterList; + } + } + + /* Count adapters. */ + size_t adapter_count = 0; + for (struct tap_adapter_node *pAdapter = pAdapterList; pAdapter; pAdapter = pAdapter->pNext) + { + adapter_count++; + } + + /* Prepare semicolon delimited list of TAP adapter ID(s) and active TAP adapter ID(s). */ + LPTSTR + szAdapters = (LPTSTR)malloc(adapter_count * (38 /*GUID*/ + 1 /*separator/terminator*/) * sizeof(TCHAR)), + szAdaptersTail = szAdapters; + if (szAdapters == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, adapter_count * (38 /*GUID*/ + 1 /*separator/terminator*/) * sizeof(TCHAR)); + uiResult = ERROR_OUTOFMEMORY; goto cleanup_pAdapterAdresses; + } + + LPTSTR + szAdaptersActive = (LPTSTR)malloc(adapter_count * (38 /*GUID*/ + 1 /*separator/terminator*/) * sizeof(TCHAR)), + szAdaptersActiveTail = szAdaptersActive; + if (szAdaptersActive == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, adapter_count * (38 /*GUID*/ + 1 /*separator/terminator*/) * sizeof(TCHAR)); + uiResult = ERROR_OUTOFMEMORY; goto cleanup_szAdapters; + } + + for (struct tap_adapter_node *pAdapter = pAdapterList; pAdapter; pAdapter = pAdapter->pNext) + { + /* Convert adapter GUID to UTF-16 string. (LPOLESTR defaults to LPWSTR) */ + LPOLESTR szAdapterId = NULL; + StringFromIID((REFIID)&pAdapter->guid, &szAdapterId); + + /* Append to the list of TAP adapter ID(s). */ + if (szAdapters < szAdaptersTail) + { + *(szAdaptersTail++) = TEXT(';'); + } + memcpy(szAdaptersTail, szAdapterId, 38 * sizeof(TCHAR)); + szAdaptersTail += 38; + + /* If this adapter is active (connected), add it to the list of active TAP adapter ID(s). */ + for (PIP_ADAPTER_ADDRESSES p = pAdapterAdresses; p; p = p->Next) + { + OLECHAR szId[38 /*GUID*/ + 1 /*terminator*/]; + GUID guid; + if (MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, p->AdapterName, -1, szId, _countof(szId)) > 0 + && SUCCEEDED(IIDFromString(szId, &guid)) + && memcmp(&guid, &pAdapter->guid, sizeof(GUID)) == 0) + { + if (p->OperStatus == IfOperStatusUp) + { + /* This TAP adapter is active (connected). */ + if (szAdaptersActive < szAdaptersActiveTail) + { + *(szAdaptersActiveTail++) = TEXT(';'); + } + memcpy(szAdaptersActiveTail, szAdapterId, 38 * sizeof(TCHAR)); + szAdaptersActiveTail += 38; + } + break; + } + } + CoTaskMemFree(szAdapterId); + } + szAdaptersTail [0] = 0; + szAdaptersActiveTail[0] = 0; + + /* Set Installer properties. */ + uiResult = MsiSetProperty(hInstall, szAdaptersPropertyName, szAdapters); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"%s\") failed", __FUNCTION__, szAdaptersPropertyName); + goto cleanup_szAdaptersActive; + } + uiResult = MsiSetProperty(hInstall, szActiveAdaptersPropertyName, szAdaptersActive); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"%s\") failed", __FUNCTION__, szActiveAdaptersPropertyName); + goto cleanup_szAdaptersActive; + } + +cleanup_szAdaptersActive: + free(szAdaptersActive); +cleanup_szAdapters: + free(szAdapters); +cleanup_pAdapterAdresses: + free(pAdapterAdresses); +cleanup_pAdapterList: + tap_free_adapter_list(pAdapterList); + return uiResult; +} + + +UINT __stdcall +FindSystemInfo(_In_ MSIHANDLE hInstall) +{ +#ifdef _MSC_VER +#pragma comment(linker, DLLEXP_EXPORT) +#endif + + debug_popup(TEXT(__FUNCTION__)); + + BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL)); + + OPENVPNMSICA_SAVE_MSI_SESSION(hInstall); + + set_openvpnserv_state(hInstall); + find_adapters( + hInstall, + TEXT("root\\") TEXT(TAP_WIN_COMPONENT_ID) TEXT("\0") TEXT(TAP_WIN_COMPONENT_ID) TEXT("\0"), + TEXT("TAPWINDOWS6ADAPTERS"), + TEXT("ACTIVETAPWINDOWS6ADAPTERS")); + find_adapters( + hInstall, + TEXT("Wintun") TEXT("\0"), + TEXT("WINTUNADAPTERS"), + TEXT("ACTIVEWINTUNADAPTERS")); + + if (bIsCoInitialized) + { + CoUninitialize(); + } + return ERROR_SUCCESS; +} + + +UINT __stdcall +CloseOpenVPNGUI(_In_ MSIHANDLE hInstall) +{ +#ifdef _MSC_VER +#pragma comment(linker, DLLEXP_EXPORT) +#endif + UNREFERENCED_PARAMETER(hInstall); /* This CA is does not interact with MSI session (report errors, access properties, tables, etc.). */ + + debug_popup(TEXT(__FUNCTION__)); + + /* Find OpenVPN GUI window. */ + HWND hWnd = FindWindow(TEXT("OpenVPN-GUI"), NULL); + if (hWnd) + { + /* Ask it to close and wait for 100ms. Unfortunately, this will succeed only for recent OpenVPN GUI that do not run elevated. */ + SendMessage(hWnd, WM_CLOSE, 0, 0); + Sleep(100); + } + + return ERROR_SUCCESS; +} + + +UINT __stdcall +StartOpenVPNGUI(_In_ MSIHANDLE hInstall) +{ +#ifdef _MSC_VER +#pragma comment(linker, DLLEXP_EXPORT) +#endif + + debug_popup(TEXT(__FUNCTION__)); + + UINT uiResult; + BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL)); + + OPENVPNMSICA_SAVE_MSI_SESSION(hInstall); + + /* Create and populate a MSI record. */ + MSIHANDLE hRecord = MsiCreateRecord(1); + if (!hRecord) + { + uiResult = ERROR_INVALID_HANDLE; + msg(M_NONFATAL, "%s: MsiCreateRecord failed", __FUNCTION__); + goto cleanup_CoInitialize; + } + uiResult = MsiRecordSetString(hRecord, 0, TEXT("\"[#bin.openvpn_gui.exe]\"")); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiRecordSetString() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiRecordSetString failed", __FUNCTION__); + goto cleanup_MsiCreateRecord; + } + + /* Format string. */ + TCHAR szStackBuf[MAX_PATH]; + DWORD dwPathSize = _countof(szStackBuf); + LPTSTR szPath = szStackBuf; + uiResult = MsiFormatRecord(hInstall, hRecord, szPath, &dwPathSize); + if (uiResult == ERROR_MORE_DATA) + { + /* Allocate buffer on heap (+1 for terminator), and retry. */ + szPath = (LPTSTR)malloc((++dwPathSize) * sizeof(TCHAR)); + if (szPath == NULL) + { + msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwPathSize * sizeof(TCHAR)); + uiResult = ERROR_OUTOFMEMORY; goto cleanup_MsiCreateRecord; + } + + uiResult = MsiFormatRecord(hInstall, hRecord, szPath, &dwPathSize); + } + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiFormatRecord() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiFormatRecord failed", __FUNCTION__); + goto cleanup_malloc_szPath; + } + + /* Launch the OpenVPN GUI. */ + SHELLEXECUTEINFO sei = { + .cbSize = sizeof(SHELLEXECUTEINFO), + .fMask = SEE_MASK_FLAG_NO_UI, /* Don't show error UI, we'll display it. */ + .lpFile = szPath, + .nShow = SW_SHOWNORMAL + }; + if (!ShellExecuteEx(&sei)) + { + uiResult = GetLastError(); + msg(M_NONFATAL | M_ERRNO, "%s: ShellExecuteEx(%s) failed", __FUNCTION__, szPath); + goto cleanup_malloc_szPath; + } + + uiResult = ERROR_SUCCESS; + +cleanup_malloc_szPath: + if (szPath != szStackBuf) + { + free(szPath); + } +cleanup_MsiCreateRecord: + MsiCloseHandle(hRecord); +cleanup_CoInitialize: + if (bIsCoInitialized) + { + CoUninitialize(); + } + return uiResult; +} + + +/** + * Schedules adapter creation. + * + * When the rollback is enabled, the adapter deletition is scheduled on rollback. + * + * @param seq The argument sequence to pass to InstallTUNTAPAdapters custom action + * + * @param seqRollback The argument sequence to pass to InstallTUNTAPAdaptersRollback custom + * action. NULL when rollback is disabled. + * + * @param szDisplayName Adapter display name + * + * @param szHardwareId Adapter hardware ID + * + * @param iTicks Pointer to an integer that represents amount of work (on progress + * indicator) the InstallTUNTAPAdapters will take. This function increments it + * by MSICA_ADAPTER_TICK_SIZE for each adapter to create. + * + * @return ERROR_SUCCESS on success; An error code otherwise + */ +static DWORD +schedule_adapter_create( + _Inout_ struct msica_arg_seq *seq, + _Inout_opt_ struct msica_arg_seq *seqRollback, + _In_z_ LPCTSTR szDisplayName, + _In_z_ LPCTSTR szHardwareId, + _Inout_ int *iTicks) +{ + /* Get existing network adapters. */ + struct tap_adapter_node *pAdapterList = NULL; + DWORD dwResult = tap_list_adapters(NULL, NULL, &pAdapterList); + if (dwResult != ERROR_SUCCESS) + { + return dwResult; + } + + /* Does adapter exist? */ + for (struct tap_adapter_node *pAdapterOther = pAdapterList;; pAdapterOther = pAdapterOther->pNext) + { + if (pAdapterOther == NULL) + { + /* No adapter with a same name found. */ + TCHAR szArgument[10 /*create=""|deleteN=""*/ + MAX_PATH /*szDisplayName*/ + 1 /*|*/ + MAX_PATH /*szHardwareId*/ + 1 /*terminator*/]; + + /* InstallTUNTAPAdapters will create the adapter. */ + _stprintf_s( + szArgument, _countof(szArgument), + TEXT("create=\"%.*s|%.*s\""), + MAX_PATH, szDisplayName, + MAX_PATH, szHardwareId); + msica_arg_seq_add_tail(seq, szArgument); + + if (seqRollback) + { + /* InstallTUNTAPAdaptersRollback will delete the adapter. */ + _stprintf_s( + szArgument, _countof(szArgument), + TEXT("deleteN=\"%.*s\""), + MAX_PATH, szDisplayName); + msica_arg_seq_add_head(seqRollback, szArgument); + } + + *iTicks += MSICA_ADAPTER_TICK_SIZE; + break; + } + else if (_tcsicmp(szDisplayName, pAdapterOther->szName) == 0) + { + /* Adapter with a same name found. */ + for (LPCTSTR hwid = pAdapterOther->szzHardwareIDs;; hwid += _tcslen(hwid) + 1) + { + if (hwid[0] == 0) + { + /* This adapter has a different hardware ID. */ + msg(M_NONFATAL, "%s: Adapter with name \"%" PRIsLPTSTR "\" already exists", __FUNCTION__, pAdapterOther->szName); + dwResult = ERROR_ALREADY_EXISTS; + goto cleanup_pAdapterList; + } + else if (_tcsicmp(hwid, szHardwareId) == 0) + { + /* This is an adapter with the requested hardware ID. We already have what we want! */ + break; + } + } + break; /* Adapter names are unique. There should be no other adapter with this name. */ + } + } + +cleanup_pAdapterList: + tap_free_adapter_list(pAdapterList); + return dwResult; +} + + +/** + * Schedules adapter deletion. + * + * When the rollback is enabled, the adapter deletition is scheduled as: disable in + * UninstallTUNTAPAdapters, enable on rollback, delete on commit. + * + * When rollback is disabled, the adapter deletition is scheduled as delete in + * UninstallTUNTAPAdapters. + * + * @param seq The argument sequence to pass to UninstallTUNTAPAdapters custom action + * + * @param seqCommit The argument sequence to pass to UninstallTUNTAPAdaptersCommit custom + * action. NULL when rollback is disabled. + * + * @param seqRollback The argument sequence to pass to UninstallTUNTAPAdaptersRollback custom + * action. NULL when rollback is disabled. + * + * @param szDisplayName Adapter display name + * + * @param szzHardwareIDs String of strings with acceptable adapter hardware IDs + * + * @param iTicks Pointer to an integer that represents amount of work (on progress + * indicator) the UninstallTUNTAPAdapters will take. This function increments + * it by MSICA_ADAPTER_TICK_SIZE for each adapter to delete. + * + * @return ERROR_SUCCESS on success; An error code otherwise + */ +static DWORD +schedule_adapter_delete( + _Inout_ struct msica_arg_seq *seq, + _Inout_opt_ struct msica_arg_seq *seqCommit, + _Inout_opt_ struct msica_arg_seq *seqRollback, + _In_z_ LPCTSTR szDisplayName, + _In_z_ LPCTSTR szzHardwareIDs, + _Inout_ int *iTicks) +{ + /* Get adapters with given hardware ID. */ + struct tap_adapter_node *pAdapterList = NULL; + DWORD dwResult = tap_list_adapters(NULL, szzHardwareIDs, &pAdapterList); + if (dwResult != ERROR_SUCCESS) + { + return dwResult; + } + + /* Does adapter exist? */ + for (struct tap_adapter_node *pAdapter = pAdapterList; pAdapter != NULL; pAdapter = pAdapter->pNext) + { + if (_tcsicmp(szDisplayName, pAdapter->szName) == 0) + { + /* Adapter found. */ + TCHAR szArgument[8 /*disable=|enable=|delete=*/ + 38 /*{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}*/ + 1 /*terminator*/]; + if (seqCommit && seqRollback) + { + /* UninstallTUNTAPAdapters will disable the adapter. */ + _stprintf_s( + szArgument, _countof(szArgument), + TEXT("disable=") TEXT(PRIXGUID), + PRIGUID_PARAM(pAdapter->guid)); + msica_arg_seq_add_tail(seq, szArgument); + + /* UninstallTUNTAPAdaptersRollback will re-enable the adapter. */ + _stprintf_s( + szArgument, _countof(szArgument), + TEXT("enable=") TEXT(PRIXGUID), + PRIGUID_PARAM(pAdapter->guid)); + msica_arg_seq_add_head(seqRollback, szArgument); + + /* UninstallTUNTAPAdaptersCommit will delete the adapter. */ + _stprintf_s( + szArgument, _countof(szArgument), + TEXT("delete=") TEXT(PRIXGUID), + PRIGUID_PARAM(pAdapter->guid)); + msica_arg_seq_add_tail(seqCommit, szArgument); + } + else + { + /* UninstallTUNTAPAdapters will delete the adapter. */ + _stprintf_s( + szArgument, _countof(szArgument), + TEXT("delete=") TEXT(PRIXGUID), + PRIGUID_PARAM(pAdapter->guid)); + msica_arg_seq_add_tail(seq, szArgument); + } + + iTicks += MSICA_ADAPTER_TICK_SIZE; + break; /* Adapter names are unique. There should be no other adapter with this name. */ + } + } + + tap_free_adapter_list(pAdapterList); + return dwResult; +} + + +UINT __stdcall +EvaluateTUNTAPAdapters(_In_ MSIHANDLE hInstall) +{ +#ifdef _MSC_VER +#pragma comment(linker, DLLEXP_EXPORT) +#endif + + debug_popup(TEXT(__FUNCTION__)); + + UINT uiResult; + BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL)); + + OPENVPNMSICA_SAVE_MSI_SESSION(hInstall); + + struct msica_arg_seq + seqInstall, + seqInstallCommit, + seqInstallRollback, + seqUninstall, + seqUninstallCommit, + seqUninstallRollback; + msica_arg_seq_init(&seqInstall); + msica_arg_seq_init(&seqInstallCommit); + msica_arg_seq_init(&seqInstallRollback); + msica_arg_seq_init(&seqUninstall); + msica_arg_seq_init(&seqUninstallCommit); + msica_arg_seq_init(&seqUninstallRollback); + + /* Check rollback state. */ + bool bRollbackEnabled = MsiEvaluateCondition(hInstall, TEXT("RollbackDisabled")) != MSICONDITION_TRUE; + + /* Open MSI database. */ + MSIHANDLE hDatabase = MsiGetActiveDatabase(hInstall); + if (hDatabase == 0) + { + msg(M_NONFATAL, "%s: MsiGetActiveDatabase failed", __FUNCTION__); + uiResult = ERROR_INVALID_HANDLE; + goto cleanup_exec_seq; + } + + /* Check if TUNTAPAdapter table exists. If it doesn't exist, there's nothing to do. */ + switch (MsiDatabaseIsTablePersistent(hDatabase, TEXT("TUNTAPAdapter"))) + { + case MSICONDITION_FALSE: + case MSICONDITION_TRUE: break; + + default: + uiResult = ERROR_SUCCESS; + goto cleanup_hDatabase; + } + + /* Prepare a query to get a list/view of adapters. */ + MSIHANDLE hViewST = 0; + LPCTSTR szQuery = TEXT("SELECT `Adapter`,`DisplayName`,`Condition`,`Component_`,`HardwareId` FROM `TUNTAPAdapter`"); + uiResult = MsiDatabaseOpenView(hDatabase, szQuery, &hViewST); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiDatabaseOpenView() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiDatabaseOpenView(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szQuery); + goto cleanup_hDatabase; + } + + /* Execute query! */ + uiResult = MsiViewExecute(hViewST, 0); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiViewExecute() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiViewExecute(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szQuery); + goto cleanup_hViewST; + } + + /* Create a record to report progress with. */ + MSIHANDLE hRecordProg = MsiCreateRecord(2); + if (!hRecordProg) + { + uiResult = ERROR_INVALID_HANDLE; + msg(M_NONFATAL, "%s: MsiCreateRecord failed", __FUNCTION__); + goto cleanup_hViewST_close; + } + + for (;; ) + { + /* Fetch one record from the view. */ + MSIHANDLE hRecord = 0; + uiResult = MsiViewFetch(hViewST, &hRecord); + if (uiResult == ERROR_NO_MORE_ITEMS) + { + uiResult = ERROR_SUCCESS; + break; + } + else if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiViewFetch() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiViewFetch failed", __FUNCTION__); + goto cleanup_hRecordProg; + } + + INSTALLSTATE iInstalled, iAction; + { + /* Read adapter component ID (`Component_` is field #4). */ + LPTSTR szValue = NULL; + uiResult = msi_get_record_string(hRecord, 4, &szValue); + if (uiResult != ERROR_SUCCESS) + { + goto cleanup_hRecord; + } + + /* Get the component state. */ + uiResult = MsiGetComponentState(hInstall, szValue, &iInstalled, &iAction); + if (uiResult != ERROR_SUCCESS) + { + SetLastError(uiResult); /* MSDN does not mention MsiGetComponentState() to set GetLastError(). But we do have an error code. Set last error manually. */ + msg(M_NONFATAL | M_ERRNO, "%s: MsiGetComponentState(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szValue); + free(szValue); + goto cleanup_hRecord; + } + free(szValue); + } + + /* Get adapter display name (`DisplayName` is field #2). */ + LPTSTR szDisplayName = NULL; + uiResult = msi_format_field(hInstall, hRecord, 2, &szDisplayName); + if (uiResult != ERROR_SUCCESS) + { + goto cleanup_hRecord; + } + /* `DisplayName` field type is [Filename](https://docs.microsoft.com/en-us/windows/win32/msi/filename), which is either "8.3|long name" or "8.3". */ + LPTSTR szDisplayNameEx = _tcschr(szDisplayName, TEXT('|')); + szDisplayNameEx = szDisplayNameEx != NULL ? szDisplayNameEx + 1 : szDisplayName; + + /* Get adapter hardware ID (`HardwareId` is field #5). */ + TCHAR szzHardwareIDs[0x100] = { 0 }; + { + LPTSTR szHwId = NULL; + uiResult = msi_get_record_string(hRecord, 5, &szHwId); + if (uiResult != ERROR_SUCCESS) + { + goto cleanup_szDisplayName; + } + memcpy_s(szzHardwareIDs, sizeof(szzHardwareIDs) - 2*sizeof(TCHAR) /*requires double zero termination*/, szHwId, _tcslen(szHwId)*sizeof(TCHAR)); + free(szHwId); + } + + if (iAction > INSTALLSTATE_BROKEN) + { + int iTicks = 0; + + if (iAction >= INSTALLSTATE_LOCAL) + { + /* Read and evaluate adapter condition (`Condition` is field #3). */ + LPTSTR szValue = NULL; + uiResult = msi_get_record_string(hRecord, 3, &szValue); + if (uiResult != ERROR_SUCCESS) + { + goto cleanup_szDisplayName; + } +#ifdef __GNUC__ +/* + * warning: enumeration value ‘MSICONDITION_TRUE’ not handled in switch + * warning: enumeration value ‘MSICONDITION_NONE’ not handled in switch + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" +#endif + switch (MsiEvaluateCondition(hInstall, szValue)) + { + case MSICONDITION_FALSE: + free(szValue); + goto cleanup_szDisplayName; + + case MSICONDITION_ERROR: + uiResult = ERROR_INVALID_FIELD; + msg(M_NONFATAL | M_ERRNO, "%s: MsiEvaluateCondition(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szValue); + free(szValue); + goto cleanup_szDisplayName; + } +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + free(szValue); + + /* Component is or should be installed. Schedule adapter creation. */ + if (schedule_adapter_create( + &seqInstall, + bRollbackEnabled ? &seqInstallRollback : NULL, + szDisplayNameEx, + szzHardwareIDs, + &iTicks) != ERROR_SUCCESS) + { + uiResult = ERROR_INSTALL_FAILED; + goto cleanup_szDisplayName; + } + } + else + { + /* Component is installed, but should be degraded to advertised/removed. Schedule adapter deletition. + * + * Note: On adapter removal (product is being uninstalled), we tolerate dwResult error. + * Better a partial uninstallation than no uninstallation at all. + */ + schedule_adapter_delete( + &seqUninstall, + bRollbackEnabled ? &seqUninstallCommit : NULL, + bRollbackEnabled ? &seqUninstallRollback : NULL, + szDisplayNameEx, + szzHardwareIDs, + &iTicks); + } + + /* Arrange the amount of tick space to add to the progress indicator. + * Do this within the loop to poll for user cancellation. */ + MsiRecordSetInteger(hRecordProg, 1, 3 /* OP3 = Add ticks to the expected total number of progress of the progress bar */); + MsiRecordSetInteger(hRecordProg, 2, iTicks); + if (MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hRecordProg) == IDCANCEL) + { + uiResult = ERROR_INSTALL_USEREXIT; + goto cleanup_szDisplayName; + } + } + +cleanup_szDisplayName: + free(szDisplayName); +cleanup_hRecord: + MsiCloseHandle(hRecord); + if (uiResult != ERROR_SUCCESS) + { + goto cleanup_hRecordProg; + } + } + + /* Store deferred custom action parameters. */ + if ((uiResult = setup_sequence(hInstall, TEXT("InstallTUNTAPAdapters" ), &seqInstall )) != ERROR_SUCCESS + || (uiResult = setup_sequence(hInstall, TEXT("InstallTUNTAPAdaptersCommit" ), &seqInstallCommit )) != ERROR_SUCCESS + || (uiResult = setup_sequence(hInstall, TEXT("InstallTUNTAPAdaptersRollback" ), &seqInstallRollback )) != ERROR_SUCCESS + || (uiResult = setup_sequence(hInstall, TEXT("UninstallTUNTAPAdapters" ), &seqUninstall )) != ERROR_SUCCESS + || (uiResult = setup_sequence(hInstall, TEXT("UninstallTUNTAPAdaptersCommit" ), &seqUninstallCommit )) != ERROR_SUCCESS + || (uiResult = setup_sequence(hInstall, TEXT("UninstallTUNTAPAdaptersRollback"), &seqUninstallRollback)) != ERROR_SUCCESS) + { + goto cleanup_hRecordProg; + } + + uiResult = ERROR_SUCCESS; + +cleanup_hRecordProg: + MsiCloseHandle(hRecordProg); +cleanup_hViewST_close: + MsiViewClose(hViewST); +cleanup_hViewST: + MsiCloseHandle(hViewST); +cleanup_hDatabase: + MsiCloseHandle(hDatabase); +cleanup_exec_seq: + msica_arg_seq_free(&seqInstall); + msica_arg_seq_free(&seqInstallCommit); + msica_arg_seq_free(&seqInstallRollback); + msica_arg_seq_free(&seqUninstall); + msica_arg_seq_free(&seqUninstallCommit); + msica_arg_seq_free(&seqUninstallRollback); + if (bIsCoInitialized) + { + CoUninitialize(); + } + return uiResult; +} + + +/** + * Parses string encoded GUID. + * + * @param szArg Zero terminated string where the GUID string starts + * + * @param guid Pointer to GUID that receives parsed value + * + * @return TRUE on success; FALSE otherwise + */ +static BOOL +parse_guid( + _In_z_ LPCWSTR szArg, + _Out_ GUID *guid) +{ + if (swscanf_s(szArg, _L(PRIXGUID), PRIGUID_PARAM_REF(*guid)) != 11) + { + msg(M_NONFATAL | M_ERRNO, "%s: swscanf_s(\"%ls\") failed", __FUNCTION__, szArg); + return FALSE; + } + return TRUE; +} + + +UINT __stdcall +ProcessDeferredAction(_In_ MSIHANDLE hInstall) +{ +#ifdef _MSC_VER +#pragma comment(linker, DLLEXP_EXPORT) +#endif + + debug_popup(TEXT(__FUNCTION__)); + + UINT uiResult; + BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL)); + + OPENVPNMSICA_SAVE_MSI_SESSION(hInstall); + + BOOL bIsCleanup = MsiGetMode(hInstall, MSIRUNMODE_COMMIT) || MsiGetMode(hInstall, MSIRUNMODE_ROLLBACK); + + /* Get sequence arguments. Always Unicode as CommandLineToArgvW() is available as Unicode-only. */ + LPWSTR szSequence = NULL; + uiResult = msi_get_string(hInstall, L"CustomActionData", &szSequence); + if (uiResult != ERROR_SUCCESS) + { + goto cleanup_CoInitialize; + } + int nArgs; + LPWSTR *szArg = CommandLineToArgvW(szSequence, &nArgs); + if (szArg == NULL) + { + uiResult = GetLastError(); + msg(M_NONFATAL | M_ERRNO, "%s: CommandLineToArgvW(\"%ls\") failed", __FUNCTION__, szSequence); + goto cleanup_szSequence; + } + + /* Tell the installer to use explicit progress messages. */ + MSIHANDLE hRecordProg = MsiCreateRecord(3); + MsiRecordSetInteger(hRecordProg, 1, 1); + MsiRecordSetInteger(hRecordProg, 2, 1); + MsiRecordSetInteger(hRecordProg, 3, 0); + MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hRecordProg); + + /* Prepare hRecordProg for progress messages. */ + MsiRecordSetInteger(hRecordProg, 1, 2); + MsiRecordSetInteger(hRecordProg, 3, 0); + + BOOL bRebootRequired = FALSE; + + for (int i = 1 /*CommandLineToArgvW injects msiexec.exe as szArg[0]*/; i < nArgs; ++i) + { + DWORD dwResult = ERROR_SUCCESS; + + if (wcsncmp(szArg[i], L"create=", 7) == 0) + { + /* Create an adapter with a given name and hardware ID. */ + LPWSTR szName = szArg[i] + 7; + LPWSTR szHardwareId = wcschr(szName, L'|'); + if (szHardwareId == NULL) + { + goto invalid_argument; + } + szHardwareId[0] = 0; + ++szHardwareId; + + { + /* Report the name of the adapter to installer. */ + MSIHANDLE hRecord = MsiCreateRecord(4); + MsiRecordSetString(hRecord, 1, TEXT("Creating adapter")); + MsiRecordSetString(hRecord, 2, szName); + MsiRecordSetString(hRecord, 3, szHardwareId); + int iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_ACTIONDATA, hRecord); + MsiCloseHandle(hRecord); + if (iResult == IDCANCEL) + { + uiResult = ERROR_INSTALL_USEREXIT; + goto cleanup; + } + } + + GUID guidAdapter; + dwResult = tap_create_adapter(NULL, NULL, szHardwareId, &bRebootRequired, &guidAdapter); + if (dwResult == ERROR_SUCCESS) + { + /* Set adapter name. */ + dwResult = tap_set_adapter_name(&guidAdapter, szName); + if (dwResult != ERROR_SUCCESS) + { + tap_delete_adapter(NULL, &guidAdapter, &bRebootRequired); + } + } + } + else if (wcsncmp(szArg[i], L"deleteN=", 8) == 0) + { + /* Delete the adapter by name. */ + LPCWSTR szName = szArg[i] + 8; + + { + /* Report the name of the adapter to installer. */ + MSIHANDLE hRecord = MsiCreateRecord(3); + MsiRecordSetString(hRecord, 1, TEXT("Deleting adapter")); + MsiRecordSetString(hRecord, 2, szName); + int iResult = MsiProcessMessage(hInstall, INSTALLMESSAGE_ACTIONDATA, hRecord); + MsiCloseHandle(hRecord); + if (iResult == IDCANCEL) + { + uiResult = ERROR_INSTALL_USEREXIT; + goto cleanup; + } + } + + /* Get existing adapters. */ + struct tap_adapter_node *pAdapterList = NULL; + dwResult = tap_list_adapters(NULL, NULL, &pAdapterList); + if (dwResult == ERROR_SUCCESS) + { + /* Does the adapter exist? */ + for (struct tap_adapter_node *pAdapter = pAdapterList; pAdapter != NULL; pAdapter = pAdapter->pNext) + { + if (_tcsicmp(szName, pAdapter->szName) == 0) + { + /* Adapter found. */ + dwResult = tap_delete_adapter(NULL, &pAdapter->guid, &bRebootRequired); + break; + } + } + + tap_free_adapter_list(pAdapterList); + } + } + else if (wcsncmp(szArg[i], L"delete=", 7) == 0) + { + /* Delete the adapter by GUID. */ + GUID guid; + if (!parse_guid(szArg[i] + 7, &guid)) + { + goto invalid_argument; + } + dwResult = tap_delete_adapter(NULL, &guid, &bRebootRequired); + } + else if (wcsncmp(szArg[i], L"enable=", 7) == 0) + { + /* Enable the adapter. */ + GUID guid; + if (!parse_guid(szArg[i] + 7, &guid)) + { + goto invalid_argument; + } + dwResult = tap_enable_adapter(NULL, &guid, TRUE, &bRebootRequired); + } + else if (wcsncmp(szArg[i], L"disable=", 8) == 0) + { + /* Disable the adapter. */ + GUID guid; + if (!parse_guid(szArg[i] + 8, &guid)) + { + goto invalid_argument; + } + dwResult = tap_enable_adapter(NULL, &guid, FALSE, &bRebootRequired); + } + else + { + goto invalid_argument; + } + + if (dwResult != ERROR_SUCCESS && !bIsCleanup /* Ignore errors in case of commit/rollback to do as much work as possible. */) + { + uiResult = ERROR_INSTALL_FAILURE; + goto cleanup; + } + + /* Report progress and check for user cancellation. */ + MsiRecordSetInteger(hRecordProg, 2, MSICA_ADAPTER_TICK_SIZE); + if (MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hRecordProg) == IDCANCEL) + { + dwResult = ERROR_INSTALL_USEREXIT; + goto cleanup; + } + + continue; + +invalid_argument: + msg(M_NONFATAL, "%s: Ignoring invalid argument: %ls", __FUNCTION__, szArg[i]); + } + +cleanup: + if (bRebootRequired) + { + MsiSetMode(hInstall, MSIRUNMODE_REBOOTATEND, TRUE); + } + MsiCloseHandle(hRecordProg); + LocalFree(szArg); +cleanup_szSequence: + free(szSequence); +cleanup_CoInitialize: + if (bIsCoInitialized) + { + CoUninitialize(); + } + return uiResult; +} diff --git a/src/openvpnmsica/openvpnmsica.h b/src/openvpnmsica/openvpnmsica.h new file mode 100644 index 0000000..221d03c --- /dev/null +++ b/src/openvpnmsica/openvpnmsica.h @@ -0,0 +1,153 @@ +/* + * openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages + * https://community.openvpn.net/openvpn/wiki/OpenVPNMSICA + * + * Copyright (C) 2018 Simon Rozman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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. + */ + +#ifndef MSICA_H +#define MSICA_H + +#include +#include +#include "../tapctl/basic.h" + + +/* + * Error codes (next unused 2552L) + */ +#define ERROR_MSICA 2550L +#define ERROR_MSICA_ERRNO 2551L + + +/** + * Thread local storage data + */ +struct openvpnmsica_thread_data +{ + MSIHANDLE hInstall; /** Handle to the installation session. */ +}; + + +/** + * MSI session handle thread local storage index + */ +extern DWORD openvpnmsica_thread_data_idx; + + +/** + * Set MSI session handle in thread local storage. + */ +#define OPENVPNMSICA_SAVE_MSI_SESSION(hInstall) \ +{ \ + struct openvpnmsica_thread_data *s = (struct openvpnmsica_thread_data *)TlsGetValue(openvpnmsica_thread_data_idx); \ + s->hInstall = (hInstall); \ +} + + +/* + * Exported DLL Functions + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __GNUC__ +#define DLLEXP_DECL __declspec(dllexport) +#else +#define DLLEXP_DECL +#define DLLEXP_EXPORT "/EXPORT:" __FUNCTION__ "=" __FUNCDNAME__ +#endif + + +/** + * Determines Windows information: + * + * - Sets `OPENVPNSERVICE` MSI property to PID of OpenVPN Service if running, or its EXE path if + * configured for auto-start. + * + * - Finds existing TAP-Windows6 adapters and set TAPWINDOWS6ADAPTERS and + * ACTIVETAPWINDOWS6ADAPTERS properties with semicolon delimited list of all installed adapter + * GUIDs and active adapter GUIDs respectively. + * + * - Finds existing Wintun adapters and set WINTUNADAPTERS and ACTIVEWINTUNADAPTERS properties + * with semicolon delimited list of all installed adapter GUIDs and active adapter GUIDs + * respectively. + * + * @param hInstall Handle to the installation provided to the DLL custom action + * + * @return ERROR_SUCCESS on success; An error code otherwise + * See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072.aspx + */ +DLLEXP_DECL UINT __stdcall +FindSystemInfo(_In_ MSIHANDLE hInstall); + + +/** + * Find OpenVPN GUI window and send it a WM_CLOSE message. + * + * @param hInstall Handle to the installation provided to the DLL custom action + * + * @return ERROR_SUCCESS on success; An error code otherwise + * See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072.aspx + */ +DLLEXP_DECL UINT __stdcall +CloseOpenVPNGUI(_In_ MSIHANDLE hInstall); + + +/** + * Launches OpenVPN GUI. It's path is obtained by expanding the `[#bin.openvpn_gui.exe]` + * therefore, its Id field in File table must be "bin.openvpn_gui.exe". + * + * @param hInstall Handle to the installation provided to the DLL custom action + * + * @return ERROR_SUCCESS on success; An error code otherwise + * See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072.aspx + */ +DLLEXP_DECL UINT __stdcall +StartOpenVPNGUI(_In_ MSIHANDLE hInstall); + + +/** + * Evaluate the TUNTAPAdapter table of the MSI package database and prepare a list of TAP + * adapters to install/remove. + * + * @param hInstall Handle to the installation provided to the DLL custom action + * + * @return ERROR_SUCCESS on success; An error code otherwise + * See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072.aspx + */ +DLLEXP_DECL UINT __stdcall +EvaluateTUNTAPAdapters(_In_ MSIHANDLE hInstall); + + +/** + * Perform scheduled deferred action. + * + * @param hInstall Handle to the installation provided to the DLL custom action + * + * @return ERROR_SUCCESS on success; An error code otherwise + * See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072.aspx + */ +DLLEXP_DECL UINT __stdcall +ProcessDeferredAction(_In_ MSIHANDLE hInstall); + +#ifdef __cplusplus +} +#endif + +#endif /* ifndef MSICA_H */ diff --git a/src/openvpnmsica/openvpnmsica.props b/src/openvpnmsica/openvpnmsica.props new file mode 100644 index 0000000..074635d --- /dev/null +++ b/src/openvpnmsica/openvpnmsica.props @@ -0,0 +1,18 @@ + + + + + + lib$(ProjectName) + + + + ..\compat;$(TAP_WINDOWS_HOME)/include;%(AdditionalIncludeDirectories) + _WIN32_WINNT=_WIN32_WINNT_VISTA;%(PreprocessorDefinitions) + + + Windows + + + + \ No newline at end of file diff --git a/src/openvpnmsica/openvpnmsica.vcxproj b/src/openvpnmsica/openvpnmsica.vcxproj new file mode 100644 index 0000000..4b42980 --- /dev/null +++ b/src/openvpnmsica/openvpnmsica.vcxproj @@ -0,0 +1,142 @@ + + + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + 15.0 + {D41AA9D6-B818-476E-992E-0E16EB86BEE2} + Win32Proj + openvpnmsica + 10.0 + + + + DynamicLibrary + true + v142 + Unicode + true + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + true + + + DynamicLibrary + false + v142 + true + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {8598c2c8-34c4-47a1-99b0-7c295a890615} + false + + + + + + \ No newline at end of file diff --git a/src/openvpnmsica/openvpnmsica.vcxproj.filters b/src/openvpnmsica/openvpnmsica.vcxproj.filters new file mode 100644 index 0000000..cb050f9 --- /dev/null +++ b/src/openvpnmsica/openvpnmsica.vcxproj.filters @@ -0,0 +1,62 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/src/openvpnmsica/openvpnmsica_resources.rc b/src/openvpnmsica/openvpnmsica_resources.rc new file mode 100644 index 0000000..1859fc3 --- /dev/null +++ b/src/openvpnmsica/openvpnmsica_resources.rc @@ -0,0 +1,62 @@ +/* + * openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages + * + * Copyright (C) 2018 Simon Rozman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include +#else +#include +#endif +#include + +#pragma code_page(65001) /* UTF8 */ + +LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL + +VS_VERSION_INFO VERSIONINFO + FILEVERSION OPENVPN_VERSION_RESOURCE + PRODUCTVERSION OPENVPN_VERSION_RESOURCE + FILEFLAGSMASK VS_FF_DEBUG | VS_FF_PRERELEASE | VS_FF_PATCHED | VS_FF_PRIVATEBUILD | VS_FF_SPECIALBUILD +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "The OpenVPN Project" + VALUE "FileDescription", "Custom Action DLL to provide OpenVPN-specific support to MSI packages" + VALUE "FileVersion", PACKAGE_VERSION ".0" + VALUE "InternalName", "OpenVPN" + VALUE "LegalCopyright", "Copyright © The OpenVPN Project" + VALUE "OriginalFilename", "libopenvpnmsica.dll" + VALUE "ProductName", "OpenVPN" + VALUE "ProductVersion", PACKAGE_VERSION ".0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END -- cgit v1.2.3