openvpn-gui/as.c

718 lines
22 KiB
C

/*
* OpenVPN-GUI -- A Windows GUI for OpenVPN.
*
* Copyright (C) 2021 Lev Stipakov <lstipakov@gmail.com>
*
* 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 <windows.h>
#include <wininet.h>
#include <stdlib.h>
#include <shlwapi.h>
#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;
WCHAR *ctx = NULL;
pch = wcstok_s(buf, L"\r\n", &ctx);
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_s(NULL, L"\r\n", &ctx);
}
/* 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, &param->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=<name> */
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, "<Message>CRV1:");
char* msg_end = strstr(buf, "</Message>");
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);
}