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