mirror of https://github.com/OpenVPN/openvpn-gui
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
parent
d6a622a023
commit
c7beb04ff5
|
@ -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})
|
||||
|
||||
|
|
|
@ -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
361
as.c
|
@ -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
6
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;
|
||||
|
|
7
misc.h
7
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue