URL profile import: download and import profile

Use WinInet to download profile into memory buffer.
If there are certain certificate errors (invalid CN,
wrong date, unknown CA, revocation check failed),
ask if user wants to continue.

Extract profile name from content, sanitize name and
save profile in temp directory. Then import profile
using existing facilities.

Signed-off-by: Lev Stipakov <lev@openvpn.net>
pull/446/head
Lev Stipakov 2021-06-29 13:44:56 +03:00 committed by Selva Nair
parent d6a622a023
commit c7beb04ff5
7 changed files with 378 additions and 10 deletions

View File

@ -27,7 +27,9 @@ add_executable(${PROJECT_NAME} WIN32
res/openvpn-gui-res.rc)
find_package(OpenSSL REQUIRED)
target_link_libraries(${PROJECT_NAME} OpenSSL::SSL
target_link_libraries(${PROJECT_NAME}
OpenSSL::SSL
Wtsapi32.lib
Netapi32.lib
ws2_32.lib
@ -40,7 +42,8 @@ target_link_libraries(${PROJECT_NAME} OpenSSL::SSL
Shell32.lib
Gdi32.lib
Comdlg32.lib
Ole32.lib)
Ole32.lib
Wininet.lib)
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})

View File

@ -94,6 +94,7 @@ openvpn_gui_SOURCES = \
save_pass.c save_pass.h \
env_set.c env_set.h \
echo.c echo.h \
as.c as.h \
openvpn-gui-res.h
openvpn_gui_LDFLAGS = -mwindows
@ -108,7 +109,8 @@ openvpn_gui_LDADD = \
-lnetapi32 \
-lole32 \
-lshlwapi \
-lsecur32
-lsecur32 \
-lwininet
openvpn-gui-res.o: $(openvpn_gui_RESOURCES) $(srcdir)/openvpn-gui-res.h
$(RCCOMPILE) -i $< -o $@

361
as.c
View File

@ -20,6 +20,7 @@
*/
#include <windows.h>
#include <wininet.h>
#include <stdlib.h>
#include "config.h"
@ -32,11 +33,341 @@
#define URL_LEN 1024
#define PROFILE_NAME_LEN 128
#define READ_CHUNK_LEN 65536
#define PROFILE_NAME_TOKEN L"# OVPN_ACCESS_SERVER_PROFILE="
#define FRIENDLY_NAME_TOKEN L"# OVPN_ACCESS_SERVER_FRIENDLY_NAME="
/**
* Extract profile name from profile content.
*
* Profile name is either (sorted in priority order):
* - value of OVPN_ACCESS_SERVER_FRIENDLY_NAME
* - value of OVPN_ACCESS_SERVER_PROFILE
* - URL
*
* @param profile profile content
* @param default_name default name for profile if it doesn't contain name
* @param out_name extracted profile name
* @param out_name_length max length of out_name char array
*/
void
ExtractProfileName(const WCHAR *profile, const WCHAR *default_name, WCHAR *out_name, size_t out_name_length)
{
WCHAR friendly_name[PROFILE_NAME_LEN] = { 0 };
WCHAR profile_name[PROFILE_NAME_LEN] = { 0 };
/* wcstok() modifies string, need to make a copy */
WCHAR *buf = _wcsdup(profile);
WCHAR *pch = NULL;
pch = wcstok(buf, L"\r\n");
while (pch != NULL) {
if (wcsbegins(pch, PROFILE_NAME_TOKEN)) {
wcsncpy(profile_name, pch + wcslen(PROFILE_NAME_TOKEN), PROFILE_NAME_LEN - 1);
profile_name[PROFILE_NAME_LEN - 1] = L'\0';
}
else if (wcsbegins(pch, FRIENDLY_NAME_TOKEN)) {
wcsncpy(friendly_name, pch + wcslen(FRIENDLY_NAME_TOKEN), PROFILE_NAME_LEN - 1);
friendly_name[PROFILE_NAME_LEN - 1] = L'\0';
}
pch = wcstok(NULL, L"\r\n");
}
/* we use .ovpn here, but extension could be customized */
/* actual extension will be applied during import */
if (wcslen(friendly_name) > 0)
swprintf(out_name, out_name_length, L"%ls.ovpn", friendly_name);
else if (wcslen(profile_name) > 0)
swprintf(out_name, out_name_length, L"%ls.ovpn", profile_name);
else
swprintf(out_name, out_name_length, L"%ls.ovpn", default_name);
out_name[out_name_length - 1] = L'\0';
/* sanitize profile name */
while (*out_name) {
wchar_t c = *out_name;
if (c == L'<' || c == L'>' || c == L':' || c == L'\"' || c == L'/' || c == L'\\' || c == L'|' || c == L'?' || c == L'*')
*out_name = L'_';
++out_name;
}
free(buf);
}
void
ShowWinInetError(HANDLE hWnd)
{
WCHAR err[256] = { 0 };
FormatMessageW(FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_FROM_SYSTEM, GetModuleHandleW(L"wininet.dll"),
GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), err, _countof(err), NULL);
ShowLocalizedMsgEx(MB_OK, hWnd, _T(PACKAGE_NAME), IDS_ERR_URL_IMPORT_PROFILE, GetLastError(), err);
}
struct UrlComponents
{
int port;
WCHAR host[URL_LEN];
bool https;
};
/**
* Extracts protocol, port and hostname from URL
*
* @param url URL to parse, length must be less than URL_MAX
*/
void
ParseUrl(const WCHAR *url, struct UrlComponents* comps)
{
ZeroMemory(comps, sizeof(struct UrlComponents));
int domain_off = 0;
comps->port = 443;
comps->https = true;
if (wcsbegins(url, L"http://")) {
domain_off = 7;
} else if (wcsbegins(url, L"https://")) {
domain_off = 8;
}
WCHAR* strport = wcsstr(url + domain_off, L":");
if (strport) {
WCHAR* end;
wcsncpy(comps->host, url + domain_off, strport - url - domain_off);
comps->port = wcstol(strport + 1, &end, 10);
}
else {
wcscpy(comps->host, url + domain_off);
}
if (comps->host[wcslen(comps->host) - 1] == L'/')
comps->host[wcslen(comps->host) - 1] = L'\0';
}
/**
* Download profile content. In case of error displays error message.
*
* @param hWnd handle of window which initiated download
* @param hRequest WinInet request handle
* @param pbuf pointer to a buffer, will be allocated by this function. Caller must free it after use.
* @param psize pointer to a profile size, assigned by this function
*/
BOOL
DownloadProfileContent(HANDLE hWnd, HINTERNET hRequest, char** pbuf, size_t* psize)
{
size_t pos = 0;
size_t size = READ_CHUNK_LEN;
*pbuf = calloc(1, size + 1);
char* buf = *pbuf;
if (buf == NULL) {
MessageBoxW(hWnd, L"Out of memory", _T(PACKAGE_NAME), MB_OK);
return FALSE;
}
while (true) {
DWORD bytesRead = 0;
if (!InternetReadFile(hRequest, buf + pos, READ_CHUNK_LEN, &bytesRead)) {
ShowWinInetError(hWnd);
return FALSE;
}
buf[pos + bytesRead] = '\0';
if (bytesRead == 0) {
size = pos;
break;
}
if (pos + bytesRead >= size) {
size += READ_CHUNK_LEN;
*pbuf = realloc(*pbuf, size + 1);
if (!*pbuf) {
free(buf);
MessageBoxW(hWnd, L"Out of memory", _T(PACKAGE_NAME), MB_OK);
return FALSE;
}
buf = *pbuf;
}
pos += bytesRead;
}
*psize = size;
return TRUE;
}
/**
* Download profile from AS and save it to a special-named temp file
*
* @param hWnd handle of window which initiated download
* @param host AS hostname, entered by user, might contain protocol and port
* @param username UTF-8 encoded username used for HTTP basic auth
* @param password UTF-8 encoded password used for HTTP basic auth
* @param autologin should autologin profile be used
* @param out_path full path to where profile is downloaded. Value assigned by this function.
* @param out_path_size number of elements in out_path arrray
*/
BOOL
DownloadProfile(HANDLE hWnd, const WCHAR *host, const char *username, const char *password_orig,
BOOL autologin, WCHAR *out_path, size_t out_path_size)
{
HANDLE hInternet = NULL;
HANDLE hConnect = NULL;
HANDLE hRequest = NULL;
BOOL result = FALSE;
char* buf = NULL;
char password[USER_PASS_LEN] = { 0 };
strncpy(password, password_orig, USER_PASS_LEN - 1);
password[USER_PASS_LEN - 1] = '\0';
hInternet = InternetOpenW(L"openvpn-gui/1.0", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
if (!hInternet) {
ShowWinInetError(hWnd);
goto done;
}
struct UrlComponents comps = { 0 };
ParseUrl(host, &comps);
/* wait cursor will be automatically reverted later */
SetCursor(LoadCursorW(0, IDC_WAIT));
hConnect = InternetConnectW(hInternet, comps.host, comps.port, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
if (!hConnect) {
ShowWinInetError(hWnd);
goto done;
}
WCHAR obj_name[URL_LEN] = { 0 };
swprintf(obj_name, URL_LEN, L"/rest/%ls?tls-cryptv2=1&action=import", autologin ? L"GetAutologin" : L"GetUserlogin");
obj_name[URL_LEN - 1] = L'\0';
hRequest = HttpOpenRequestW(hConnect, NULL, obj_name, NULL, NULL, NULL, comps.https ? INTERNET_FLAG_SECURE : 0, 0);
if (!hRequest) {
ShowWinInetError(hWnd);
goto done;
}
again:
if (buf) {
free(buf);
buf = NULL;
}
/* turns out that *A WinAPI function must be used with UTF-8 encoded parameters to get
* correct Base64 encoding (used by Basic HTTP auth) for non-ASCII characters
*/
InternetSetOptionA(hRequest, INTERNET_OPTION_USERNAME, (LPVOID)username, (DWORD)strlen(username));
InternetSetOptionA(hRequest, INTERNET_OPTION_PASSWORD, (LPVOID)password, (DWORD)strlen(password));
/* handle cert errors */
/* https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/182888 */
if (!HttpSendRequestW(hRequest, NULL, 0, NULL, 0)) {
DWORD err = GetLastError();
if ((err == ERROR_INTERNET_INVALID_CA) ||
(err == ERROR_INTERNET_SEC_CERT_CN_INVALID) ||
(err == ERROR_INTERNET_SEC_CERT_DATE_INVALID) ||
(err == ERROR_INTERNET_SEC_CERT_REV_FAILED)) {
/* ask user what to do and modify options if needed */
DWORD dlg_result = InternetErrorDlg(hWnd, hRequest,
err,
FLAGS_ERROR_UI_FILTER_FOR_ERRORS |
FLAGS_ERROR_UI_FLAGS_GENERATE_DATA |
FLAGS_ERROR_UI_FLAGS_CHANGE_OPTIONS,
NULL);
if (dlg_result == ERROR_SUCCESS) {
/* for unknown reasons InternetErrorDlg() doesn't change options for ERROR_INTERNET_SEC_CERT_REV_FAILED,
* despite user is willing to continue, so we have to do it manually */
if (err == ERROR_INTERNET_SEC_CERT_REV_FAILED) {
DWORD flags;
DWORD len = sizeof(flags);
InternetQueryOption(hRequest, INTERNET_OPTION_SECURITY_FLAGS, (LPVOID)&flags, &len);
flags |= SECURITY_FLAG_IGNORE_REVOCATION;
InternetSetOption(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &flags, sizeof(flags));
goto again;
}
SetCursor(LoadCursorW(0, IDC_WAIT));
goto again;
}
else
goto done;
}
ShowWinInetError(hWnd);
goto done;
}
/* get http status code */
DWORD statusCode = 0;
DWORD length = sizeof(DWORD);
HttpQueryInfoW(hRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &statusCode, &length, NULL);
if (statusCode != 200) {
ShowLocalizedMsgEx(MB_OK, hWnd, _T(PACKAGE_NAME), IDS_ERR_URL_IMPORT_PROFILE, statusCode, "HTTP error");
goto done;
}
/* download profile content */
size_t size = 0;
if (!DownloadProfileContent(hWnd, hRequest, &buf, &size))
goto done;
WCHAR name[MAX_PATH] = {0};
WCHAR* wbuf = Widen(buf);
if (!wbuf) {
MessageBoxW(hWnd, L"Failed to convert profile content to wchar", _T(PACKAGE_NAME), MB_OK);
goto done;
}
ExtractProfileName(wbuf, comps.host, name, MAX_PATH);
free(wbuf);
/* save profile content into tmp file */
DWORD res = GetTempPathW((DWORD)out_path_size, out_path);
if (res == 0 || res > out_path_size) {
MessageBoxW(hWnd, L"Failed to get TMP path", _T(PACKAGE_NAME), MB_OK);
goto done;
}
swprintf(out_path, out_path_size, L"%s%s", out_path, name);
out_path[out_path_size - 1] = '\0';
FILE* f = _wfopen(out_path, L"w");
if (f == NULL) {
MessageBoxW(hWnd, L"Unable to save downloaded profile", _T(PACKAGE_NAME), MB_OK);
goto done;
}
fwrite(buf, sizeof(char), size, f);
fclose(f);
result = TRUE;
done:
if (buf)
free(buf);
if (hRequest)
InternetCloseHandle(hRequest);
if (hConnect)
InternetCloseHandle(hConnect);
if (hInternet)
InternetCloseHandle(hInternet);
return result;
}
INT_PTR CALLBACK
ImportProfileFromURLDialogFunc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
WCHAR url[URL_LEN] = {0};
BOOL autologin = FALSE;
switch (msg)
{
case WM_INITDIALOG:
@ -53,8 +384,7 @@ ImportProfileFromURLDialogFunc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lPa
case ID_EDT_AUTH_USER:
case ID_EDT_AUTH_PASS:
case ID_EDT_URL:
if (HIWORD(wParam) == EN_UPDATE)
{
if (HIWORD(wParam) == EN_UPDATE) {
/* enable OK button only if url and username are filled */
BOOL enableOK = GetWindowTextLengthW(GetDlgItem(hwndDlg, ID_EDT_URL))
&& GetWindowTextLengthW(GetDlgItem(hwndDlg, ID_EDT_AUTH_USER));
@ -63,6 +393,33 @@ ImportProfileFromURLDialogFunc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lPa
break;
case IDOK:
autologin = IsDlgButtonChecked(hwndDlg, ID_CHK_AUTOLOGIN) == BST_CHECKED;
GetDlgItemTextW(hwndDlg, ID_EDT_URL, url, _countof(url));
int username_len = 0;
char *username = NULL;
GetDlgItemTextUtf8(hwndDlg, ID_EDT_AUTH_USER, &username, &username_len);
int password_len = 0;
char *password = NULL;
GetDlgItemTextUtf8(hwndDlg, ID_EDT_AUTH_PASS, &password, &password_len);
WCHAR path[MAX_PATH + 1] = { 0 };
BOOL downloaded = DownloadProfile(hwndDlg, url, username, password, autologin, path, _countof(path));
if (username_len != 0)
free(username);
if (password_len != 0)
free(password);
if (downloaded) {
EndDialog(hwndDlg, LOWORD(wParam));
ImportConfigFile(path);
_wunlink(path);
}
return TRUE;
case IDCANCEL:

6
misc.c
View File

@ -122,11 +122,7 @@ Base64Decode(const char *input, char **output)
return len;
}
/*
* Helper function to convert UCS-2 text from a dialog item to UTF-8.
* Caller must free *str if *len != 0.
*/
static BOOL
BOOL
GetDlgItemTextUtf8(HWND hDlg, int id, LPSTR *str, int *len)
{
int ucs2_len, utf8_len;

7
misc.h
View File

@ -73,4 +73,11 @@ BOOL open_url(const wchar_t *url);
void ImportConfigFile(const TCHAR* path);
/*
* Helper function to convert UCS-2 text from a dialog item to UTF-8.
* Caller must free *str if *len != 0.
*/
BOOL
GetDlgItemTextUtf8(HWND hDlg, int id, LPSTR* str, int* len);
#endif

View File

@ -148,6 +148,7 @@
#define ID_DLG_URL_PROFILE_IMPORT 400
#define ID_EDT_URL 401
#define ID_CHK_AUTOLOGIN 402
#define IDS_ERR_URL_IMPORT_PROFILE 403
/*
* String Table Resources

View File

@ -536,4 +536,6 @@ once as Administrator to update the registry."
IDS_NFO_CLICK_HERE_TO_START "OpenVPN GUI is already running. Right click on the tray icon to start."
IDS_NFO_BYTECOUNT "Bytes in: %s out: %s"
/* AS profile import */
IDS_ERR_URL_IMPORT_PROFILE "Error fetching profile from URL: [%d] %ls"
END