You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

446 lines
14 KiB

/*
* 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 "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="
/**
* 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:
SetStatusWinIcon(hwndDlg, ID_ICO_APP);
/* disable OK button by default - not disabled in resources */
EnableWindow(GetDlgItem(hwndDlg, IDOK), FALSE);
break;
case WM_COMMAND:
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:
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:
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, 0);
}