/* * OpenVPN-GUI -- A Windows GUI for OpenVPN. * * Copyright (C) 2021 Lev Stipakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program (see the file COPYING included with this * distribution); if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include "config.h" #include "localization.h" #include "main.h" #include "misc.h" #include "openvpn.h" #include "openvpn-gui-res.h" #include "save_pass.h" #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=" /** Replace characters not allowed in Windows filenames with '_' */ void SanitizeFilename(wchar_t *fname) { const wchar_t *reserved = L"<>:\"/\\|?*;"; /* remap these and ascii 1 to 31 */ while (*fname) { wchar_t c = *fname; if (c < 32 || wcschr(reserved, c)) { *fname = L'_'; } ++fname; } } /** * 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 * - specified default_name * * @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'; SanitizeFilename(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]; WCHAR path[URL_LEN]; char content_type[256]; 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)); comps->port = 443; comps->https = true; if (wcsbegins(url, L"http://")) { url += 7; } else if (wcsbegins(url, L"https://")) { url +=8; } WCHAR *strport = wcsstr(url, L":"); WCHAR *pathptr = wcsstr(url, L"/"); if (strport) { wcsncpy_s(comps->host, URL_LEN, url, strport - url); comps->port = wcstol(strport + 1, NULL, 10); } else if (pathptr) { wcsncpy_s(comps->host, URL_LEN, url, pathptr - url); } else { wcsncpy_s(comps->host, URL_LEN, url, _TRUNCATE); } if (pathptr) { wcsncpy_s(comps->path, URL_LEN, pathptr + 1, _TRUNCATE); } } /** * 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; } /* * DialogProc for challenge-response */ INT_PTR CALLBACK CRDialogFunc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam) { auth_param_t * param = NULL; switch (msg) { case WM_INITDIALOG: param = (auth_param_t*)lParam; SetProp(hwndDlg, cfgProp, (HANDLE)param); WCHAR *wstr = Widen(param->str); if (!wstr) { EndDialog(hwndDlg, LOWORD(wParam)); break; } SetDlgItemTextW(hwndDlg, ID_TXT_DESCRIPTION, wstr); free(wstr); /* Set password echo on if needed */ if (param->flags & FLAG_CR_ECHO) SendMessage(GetDlgItem(hwndDlg, ID_EDT_RESPONSE), EM_SETPASSWORDCHAR, 0, 0); SetForegroundWindow(hwndDlg); /* disable OK button by default - not disabled in resources */ EnableWindow(GetDlgItem(hwndDlg, IDOK), FALSE); break; case WM_COMMAND: param = (auth_param_t*)GetProp(hwndDlg, cfgProp); switch (LOWORD(wParam)) { case ID_EDT_RESPONSE: if (HIWORD(wParam) == EN_UPDATE) { /* enable OK if response is non-empty */ BOOL enableOK = GetWindowTextLength((HWND)lParam); EnableWindow(GetDlgItem(hwndDlg, IDOK), enableOK); } break; case IDOK: { int len = 0; GetDlgItemTextUtf8(hwndDlg, ID_EDT_RESPONSE, ¶m->cr_response, &len); EndDialog(hwndDlg, LOWORD(wParam)); } return TRUE; case IDCANCEL: EndDialog(hwndDlg, LOWORD(wParam)); return TRUE; } break; case WM_CLOSE: EndDialog(hwndDlg, LOWORD(wParam)); return TRUE; case WM_NCDESTROY: param = (auth_param_t*)GetProp(hwndDlg, cfgProp); RemoveProp(hwndDlg, cfgProp); break; } return FALSE; } /** * Construct the REST URL for AS profile * * @param host AS hostname, entered by user, might contain protocol and port * @param autologin should autologin profile be used * @param comps Pointer to UrlComponents. Value assigned by this function. */ static void GetASUrl(const WCHAR *host, bool autologin, struct UrlComponents *comps) { ParseUrl(host, comps); swprintf(comps->path, URL_LEN, L"/rest/%ls?tls-cryptv2=1&action=import", autologin ? L"GetAutologin" : L"GetUserlogin"); comps->path[URL_LEN - 1] = L'\0'; } /** * Read content-disposition header and extract file name if any. * Returns true on success, false otherwise. */ bool ExtractFilenameFromHeader(HINTERNET hRequest, wchar_t *name, size_t len) { DWORD index = 0; char *buf = NULL; DWORD buflen = 256; bool res = false; UINT codepage = 28591; /* ISO 8859_1 -- the default char set for http header */ buf = malloc(buflen); if (!buf || (!HttpQueryInfoA(hRequest, HTTP_QUERY_CONTENT_DISPOSITION, buf, &buflen, &index) && GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { goto done; } if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { /* try again with more space */ free(buf); buf = malloc(buflen); if (!buf || !HttpQueryInfoA(hRequest, HTTP_QUERY_CONTENT_DISPOSITION, buf, &buflen, &index)) { goto done; } } /* look for filename= */ char *p = strtok(buf, ";"); char *fn = NULL; for ( ; p; p = strtok(NULL, ";")) { if ((fn = strstr(p, "filename=")) != NULL) { fn += 9; continue; } else if ((fn = strstr(p, "filename*=utf-8''")) != NULL) { fn += 17; UrlUnescapeA(fn, NULL, NULL, URL_UNESCAPE_INPLACE); codepage = CP_UTF8; break; /* we prefer filename*= value */ } } if (fn && strlen(fn)) { StrTrimA(fn, " \""); /* strip leading and trailing spaces and quotes */ wchar_t *wfn = WidenEx(codepage, fn); if (wfn) { wcsncpy_s(name, len, wfn, _TRUNCATE); res = true; free(wfn); } } SanitizeFilename(name); done: free(buf); return res; } /** * Download profile from a generic URL and save it to a temp file * * @param hWnd handle of window which initiated download * @param comps pointer to struct UrlComponents describing the URL * @param username UTF-8 encoded username used for HTTP basic auth * @param password UTF-8 encoded password used for HTTP basic auth * @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 * * Filename in out_path is parsed from tags in received data * with the url hostname as a fallback. */ static BOOL DownloadProfile(HANDLE hWnd, const struct UrlComponents *comps, const char *username, const char *password_orig, WCHAR *out_path, size_t out_path_size) { HANDLE hInternet = NULL; HANDLE hConnect = NULL; HANDLE hRequest = NULL; BOOL result = FALSE; char* buf = NULL; /* need to make copy of password to use it for dynamic response */ char password[USER_PASS_LEN] = { 0 }; strncpy_s(password, _countof(password), password_orig, _TRUNCATE); /* empty password causes reuse of previously cached value -- set it to some character */ if (strlen(password) == 0) { password[0] = 'x'; } hInternet = InternetOpenW(L"openvpn-gui/1.0", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (!hInternet) { ShowWinInetError(hWnd); goto done; } /* Calls to connect and receive block: set timeouts that are not too long */ unsigned long timeout = 30000; /* 30 seconds */ InternetSetOption(hInternet, INTERNET_OPTION_CONNECT_TIMEOUT, &timeout, sizeof(timeout)); InternetSetOption(hInternet, INTERNET_OPTION_RECEIVE_TIMEOUT, &timeout, sizeof(timeout)); /* 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; } DWORD req_flags = INTERNET_FLAG_RELOAD; /* load from server, do not use cached data */ req_flags |= comps->https ? INTERNET_FLAG_SECURE : 0; hRequest = HttpOpenRequestW(hConnect, NULL, comps->path, NULL, NULL, NULL, req_flags, 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)) { #ifdef DEBUG 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); InternetQueryOptionW(hRequest, INTERNET_OPTION_SECURITY_FLAGS, (LPVOID)&flags, &len); flags |= SECURITY_FLAG_IGNORE_REVOCATION; InternetSetOptionW(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &flags, sizeof(flags)); goto again; } SetCursor(LoadCursorW(0, IDC_WAIT)); goto again; } else goto done; } #endif ShowWinInetError(hWnd); goto done; } /* get http status code */ DWORD status_code = 0; DWORD length = sizeof(DWORD); HttpQueryInfoW(hRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &status_code, &length, NULL); size_t size = 0; /* download profile content */ if ((status_code == 200) || (status_code == 401)) { if (!DownloadProfileContent(hWnd, hRequest, &buf, &size)) goto done; char* msg_begin = strstr(buf, "CRV1:"); char* msg_end = strstr(buf, ""); if ((status_code == 401) && msg_begin && msg_end) { *msg_end = '\0'; auth_param_t* param = (auth_param_t*)calloc(1, sizeof(auth_param_t)); if (!param) goto done; if (parse_dynamic_cr(msg_begin + 14, param)) { /* prompt user for dynamic challenge */ INT_PTR res = LocalizedDialogBoxParam(ID_DLG_CHALLENGE_RESPONSE, CRDialogFunc, (LPARAM)param); if (res == IDOK) _snprintf_0(password, "CRV1::%s::%s", param->id, param->cr_response); free_auth_param(param); if (res == IDOK) goto again; } else { free_auth_param(param); } } } if (status_code != 200) { ShowLocalizedMsgEx(MB_OK, hWnd, _T(PACKAGE_NAME), IDS_ERR_URL_IMPORT_PROFILE, status_code, L"HTTP error"); goto done; } /* check content-type if specified */ if (strlen(comps->content_type) > 0) { char tmp[256]; DWORD len = sizeof(tmp); BOOL res = HttpQueryInfoA(hRequest, HTTP_QUERY_CONTENT_TYPE, tmp, &len, NULL); if (!res || stricmp(comps->content_type, tmp)) { ShowLocalizedMsgEx(MB_OK, hWnd, _T(PACKAGE_NAME), IDS_ERR_URL_IMPORT_PROFILE, 0, L"HTTP content-type mismatch"); goto done; } } WCHAR name[MAX_PATH] = {0}; /* read filename from header or from the profile metadata */ if (strlen(comps->content_type) == 0 /* AS profile */ || !ExtractFilenameFromHeader(hRequest, name, MAX_PATH)) { 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"%ls%ls", 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); /* wipe the password */ SecureZeroMemory(password, sizeof(password)); if (hRequest) InternetCloseHandle(hRequest); if (hConnect) InternetCloseHandle(hConnect); if (hInternet) InternetCloseHandle(hInternet); return result; } typedef enum { server_as = 1, server_generic = 2 } server_type_t; INT_PTR CALLBACK ImportProfileFromURLDialogFunc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam) { WCHAR url[URL_LEN] = {0}; BOOL autologin = FALSE; server_type_t type; switch (msg) { case WM_INITDIALOG: type = (server_type_t) lParam; SetProp(hwndDlg, cfgProp, (HANDLE)lParam); SetStatusWinIcon(hwndDlg, ID_ICO_APP); if (type == server_generic) { /* Change window title and hide autologin checkbox */ SetWindowTextW(hwndDlg, LoadLocalizedString(IDS_MENU_IMPORT_URL)); ShowWindow(GetDlgItem(hwndDlg, ID_CHK_AUTOLOGIN), SW_HIDE); } /* disable OK button until required data is filled in */ EnableWindow(GetDlgItem(hwndDlg, IDOK), FALSE); break; case WM_COMMAND: type = (server_type_t) GetProp(hwndDlg, cfgProp); switch (LOWORD(wParam)) { case ID_EDT_AUTH_USER: case ID_EDT_AUTH_PASS: case ID_EDT_URL: 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)); EnableWindow(GetDlgItem(hwndDlg, IDOK), enableOK); } break; case IDOK: 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 }; struct UrlComponents comps = {0}; if (type == server_as) { autologin = IsDlgButtonChecked(hwndDlg, ID_CHK_AUTOLOGIN) == BST_CHECKED; GetASUrl(url, autologin, &comps); } else { ParseUrl(url, &comps); strncpy_s(comps.content_type, _countof(comps.content_type), "application/x-openvpn-profile", _TRUNCATE); } BOOL downloaded = DownloadProfile(hwndDlg, &comps, username, password, path, _countof(path)); if (username_len > 0) free(username); if (password_len > 0) { SecureZeroMemory(password, strlen(password)); free(password); } if (downloaded) { EndDialog(hwndDlg, LOWORD(wParam)); ImportConfigFile(path, false); /* do not prompt user */ _wunlink(path); } return TRUE; case IDCANCEL: EndDialog(hwndDlg, LOWORD(wParam)); return TRUE; } break; case WM_CLOSE: EndDialog(hwndDlg, LOWORD(wParam)); return TRUE; case WM_NCDESTROY: break; } return FALSE; } void ImportConfigFromAS() { LocalizedDialogBoxParam(ID_DLG_URL_PROFILE_IMPORT, ImportProfileFromURLDialogFunc, (LPARAM) server_as); } void ImportConfigFromURL() { LocalizedDialogBoxParam(ID_DLG_URL_PROFILE_IMPORT, ImportProfileFromURLDialogFunc, (LPARAM) server_generic); }