From c7beb04ff5a627c623652440670e4a94d47335e7 Mon Sep 17 00:00:00 2001 From: Lev Stipakov Date: Tue, 29 Jun 2021 13:44:56 +0300 Subject: [PATCH] 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 --- CMakeLists.txt | 7 +- Makefile.am | 4 +- as.c | 361 +++++++++++++++++++++++++++++++++++++- misc.c | 6 +- misc.h | 7 + openvpn-gui-res.h | 1 + res/openvpn-gui-res-en.rc | 2 + 7 files changed, 378 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5364cc7..1751ec2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/Makefile.am b/Makefile.am index 530ed7a..d694fb2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 $@ diff --git a/as.c b/as.c index 153f416..9c294f9 100644 --- a/as.c +++ b/as.c @@ -20,6 +20,7 @@ */ #include +#include #include #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: diff --git a/misc.c b/misc.c index 82d550f..b63df5e 100644 --- a/misc.c +++ b/misc.c @@ -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; diff --git a/misc.h b/misc.h index 46cd6df..93ac370 100644 --- a/misc.h +++ b/misc.h @@ -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 diff --git a/openvpn-gui-res.h b/openvpn-gui-res.h index d177f1d..e146f16 100644 --- a/openvpn-gui-res.h +++ b/openvpn-gui-res.h @@ -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 diff --git a/res/openvpn-gui-res-en.rc b/res/openvpn-gui-res-en.rc index e4e4b9b..693981f 100644 --- a/res/openvpn-gui-res-en.rc +++ b/res/openvpn-gui-res-en.rc @@ -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