/* * OpenVPN-GUI -- A Windows GUI for OpenVPN. * * Copyright (C) 2017 Selva Nair * * 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 */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include "main.h" #include "options.h" #include "misc.h" #include "openvpn.h" #include "echo.h" #include "tray.h" #include "openvpn-gui-res.h" #include "localization.h" #include "registry.h" extern options_t o; /* echo msg types */ #define ECHO_MSG_WINDOW (1) #define ECHO_MSG_NOTIFY (2) /* Old text in the window is deleted when content grows beyond this many lines */ #define MAX_MSG_LINES 1000 struct echo_msg_history { struct echo_msg_fp fp; struct echo_msg_history *next; }; /* We use a global message window for all messages */ static HWND echo_msg_window; /* Forward declarations */ static void AddMessageBoxText(HWND hwnd, const wchar_t *text, const wchar_t *title, const wchar_t *from); static INT_PTR CALLBACK MessageDialogFunc(HWND hwnd, UINT msg, UNUSED WPARAM wParam, LPARAM lParam); void echo_msg_init(void) { echo_msg_window = CreateLocalizedDialogParam(ID_DLG_MESSAGE, MessageDialogFunc, (LPARAM) 0); if (!echo_msg_window) { MsgToEventLog(EVENTLOG_ERROR_TYPE, L"Error creating echo message window."); } } /* compute a digest of the message and add it to the msg struct */ static void echo_msg_add_fp(struct echo_msg *msg, time_t timestamp) { md_ctx ctx; msg->fp.timestamp = timestamp; if (md_init(&ctx, CALG_SHA1) != 0) { return; } md_update(&ctx, (BYTE *) msg->text, msg->txtlen*sizeof(msg->text[0])); md_update(&ctx, (BYTE *) msg->title, wcslen(msg->title)*sizeof(msg->title[0])); md_final(&ctx, msg->fp.digest); return; } /* find message with given digest in history */ static struct echo_msg_history * echo_msg_recall(const BYTE *digest, struct echo_msg_history *hist) { for (; hist; hist = hist->next) { if (memcmp(hist->fp.digest, digest, HASHLEN) == 0) { break; } } return hist; } /* Add an item to message history and return the head of the list */ static struct echo_msg_history * echo_msg_history_add(struct echo_msg_history *head, const struct echo_msg_fp *fp) { struct echo_msg_history *hist = malloc(sizeof(struct echo_msg_history)); if (hist) { memcpy(&hist->fp, fp, sizeof(*fp)); hist->next = head; head = hist; } return head; } /* Save message in history -- update if already present */ static void echo_msg_save(struct echo_msg *msg) { struct echo_msg_history *hist = echo_msg_recall(msg->fp.digest, msg->history); if (hist) /* update */ { hist->fp.timestamp = msg->fp.timestamp; } else /* add */ { msg->history = echo_msg_history_add(msg->history, &msg->fp); } } /* persist echo msg history to the registry */ void echo_msg_persist(connection_t *c) { struct echo_msg_history *hist; size_t len = 0; for (hist = c->echo_msg.history; hist; hist = hist->next) { len++; if (len > 99) { break; /* max 100 history items persisted */ } } if (len == 0) { return; } size_t size = len*sizeof(struct echo_msg_fp); struct echo_msg_fp *data = malloc(size); if (data == NULL) { WriteStatusLog(c, L"GUI> ", L"Failed to persist echo msg history: Out of memory", false); return; } size_t i = 0; for (hist = c->echo_msg.history; i < len && hist; hist = hist->next) { data[i++] = hist->fp; } if (!SetConfigRegistryValueBinary(c->config_name, L"echo_msg_history", (BYTE *) data, size)) { WriteStatusLog(c, L"GUI> ", L"Failed to persist echo msg history: error writing to registry", false); } free(data); return; } /* load echo msg history from registry */ void echo_msg_load(connection_t *c) { struct echo_msg_fp *data = NULL; DWORD item_len = sizeof(struct echo_msg_fp); size_t size = GetConfigRegistryValue(c->config_name, L"echo_msg_history", NULL, 0); if (size == 0) { return; /* no history in registry */ } else if (size%item_len != 0) { WriteStatusLog(c, L"GUI> ", L"echo msg history in registry has invalid size", false); return; } data = malloc(size); if (!data || !GetConfigRegistryValue(c->config_name, L"echo_msg_history", (BYTE *) data, size)) { goto out; } size_t len = size/item_len; for (size_t i = 0; i < len; i++) { c->echo_msg.history = echo_msg_history_add(c->echo_msg.history, &data[i]); } out: free(data); } /* Return true if the message is same as recently shown */ static BOOL echo_msg_repeated(const struct echo_msg *msg) { const struct echo_msg_history *hist; hist = echo_msg_recall(msg->fp.digest, msg->history); return (hist && (hist->fp.timestamp + o.popup_mute_interval*3600 > msg->fp.timestamp)); } /* Append a line of echo msg */ static void echo_msg_append(connection_t *c, time_t UNUSED timestamp, const char *msg, BOOL addnl) { wchar_t *eol = L""; wchar_t *wmsg = NULL; if (!(wmsg = Widen(msg))) { WriteStatusLog(c, L"GUI> ", L"Error: out of memory while processing echo msg", false); goto out; } size_t len = c->echo_msg.txtlen + wcslen(wmsg) + 1; /* including null terminator */ if (addnl) { eol = L"\r\n"; len += 2; } WCHAR *s = realloc(c->echo_msg.text, len*sizeof(WCHAR)); if (!s) { WriteStatusLog(c, L"GUI> ", L"Error: out of memory while processing echo msg", false); goto out; } swprintf(s + c->echo_msg.txtlen, len - c->echo_msg.txtlen, L"%ls%ls", wmsg, eol); s[len-1] = L'\0'; c->echo_msg.text = s; c->echo_msg.txtlen = len - 1; /* exclude null terminator */ out: free(wmsg); return; } /* Called when echo msg-window or echo msg-notify is received */ static void echo_msg_display(connection_t *c, time_t timestamp, const char *title, int type) { WCHAR *wtitle = Widen(title); if (wtitle) { c->echo_msg.title = wtitle; } else { WriteStatusLog(c, L"GUI> ", L"Error: out of memory converting echo message title to widechar", false); c->echo_msg.title = L"Message from server"; } echo_msg_add_fp(&c->echo_msg, timestamp); /* add fingerprint: digest+timestamp */ /* Check whether the message is muted */ if (c->flags & FLAG_DISABLE_ECHO_MSG || echo_msg_repeated(&c->echo_msg)) { return; } if (type == ECHO_MSG_WINDOW) { DWORD_PTR res; UINT timeout = 5000; /* msec */ if (echo_msg_window && SendMessageTimeout(echo_msg_window, WM_OVPN_ECHOMSG, 0, (LPARAM) c, SMTO_BLOCK, timeout, &res) == 0) { WriteStatusLog(c, L"GUI> Failed to display echo message: ", c->echo_msg.title, false); } } else /* notify */ { ShowTrayBalloon(c->echo_msg.title, c->echo_msg.text); } /* save or update history */ echo_msg_save(&c->echo_msg); } void echo_msg_process(connection_t *c, time_t timestamp, const char *s) { wchar_t errmsg[256] = L""; char *msg = url_decode(s); if (!msg) { WriteStatusLog(c, L"GUI> ", L"Error in url_decode of echo message", false); return; } if (strbegins(msg, "msg ")) { echo_msg_append(c, timestamp, msg + 4, true); } else if (streq(msg, "msg")) /* empty msg is treated as a new line */ { echo_msg_append(c, timestamp, msg+3, true); } else if (strbegins(msg, "msg-n ")) { echo_msg_append(c, timestamp, msg + 6, false); } else if (strbegins(msg, "msg-window ")) { echo_msg_display(c, timestamp, msg + 11, ECHO_MSG_WINDOW); echo_msg_clear(c, false); } else if (strbegins(msg, "msg-notify ")) { echo_msg_display(c, timestamp, msg + 11, ECHO_MSG_NOTIFY); echo_msg_clear(c, false); } else { _sntprintf_0(errmsg, L"WARNING: Unknown ECHO directive '%hs' ignored.", msg); WriteStatusLog(c, L"GUI> ", errmsg, false); } free(msg); } void echo_msg_clear(connection_t *c, BOOL clear_history) { CLEAR(c->echo_msg.fp); free(c->echo_msg.text); free(c->echo_msg.title); c->echo_msg.text = NULL; c->echo_msg.txtlen = 0; c->echo_msg.title = NULL; if (clear_history) { echo_msg_persist(c); struct echo_msg_history *head = c->echo_msg.history; struct echo_msg_history *next; while (head) { next = head->next; free(head); head = next; } CLEAR(c->echo_msg); } } /* * Read the text from edit control h within the range specified in the * CHARRANGE structure chrg. Return the result in a newly allocated * string or NULL on error. * * The caller must free the returned pointer. */ static wchar_t * get_text_in_range(HWND h, CHARRANGE chrg) { if (chrg.cpMax <= chrg.cpMin) { return NULL; } size_t len = chrg.cpMax - chrg.cpMin; wchar_t *txt = malloc((len + 1)*sizeof(wchar_t)); if (txt) { TEXTRANGEW txtrg = {chrg, txt}; if (SendMessage(h, EM_GETTEXTRANGE, 0, (LPARAM)&txtrg) <= 0) { txt[0] = '\0'; } else { txt[len] = '\0'; /* safety */ } } return txt; } /* Enable url detection and subscribe to link click notification in an edit control */ static void enable_url_detection(HWND hmsg) { /* Recognize URLs embedded in message text */ SendMessage(hmsg, EM_AUTOURLDETECT, AURL_ENABLEURL, 0); /* Have notified by EN_LINK messages when URLs are clicked etc. */ LRESULT evmask = SendMessage(hmsg, EM_GETEVENTMASK, 0, 0); SendMessage(hmsg, EM_SETEVENTMASK, 0, evmask | ENM_LINK); } /* Open URL when ENLINK notification is received */ static int OnEnLinkNotify(HWND UNUSED hwnd, ENLINK *el) { if (el->msg == WM_LBUTTONUP) { /* get the link text */ wchar_t *url = get_text_in_range(el->nmhdr.hwndFrom, el->chrg); if (url) { open_url(url); } free(url); return 1; } return 0; } /* Add new message to the message box window */ static void AddMessageBoxText(HWND hwnd, const wchar_t *text, const wchar_t *title, const wchar_t *from) { HWND hmsg = GetDlgItem(hwnd, ID_TXT_MESSAGE); DWORD align[2] = {PFA_LEFT, PFA_RIGHT}; /* default alignments for text and title */ if (LangFlowDirection() == 1) { align[0] = PFA_RIGHT; align[1] = PFA_LEFT; } /* Start adding new message at the top */ SendMessage(hmsg, EM_SETSEL, 0, 0); CHARFORMATW cfm = {.cbSize = sizeof(CHARFORMATW) }; /* save current alignment */ PARAFORMAT pf = {.cbSize = sizeof(PARAFORMAT) }; SendMessage(hmsg, EM_GETPARAFORMAT, 0, (LPARAM) &pf); WORD pf_align_saved = pf.dwMask & PFM_ALIGNMENT ? pf.wAlignment : align[0]; pf.dwMask |= PFM_ALIGNMENT; if (from && wcslen(from)) { /* Change font to italics */ SendMessage(hmsg, EM_GETCHARFORMAT, SCF_DEFAULT, (LPARAM) &cfm); cfm.dwMask |= CFM_ITALIC; cfm.dwEffects |= CFE_ITALIC; SendMessage(hmsg, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) &cfm); /* Align to right */ pf.wAlignment = align[1]; SendMessage(hmsg, EM_SETPARAFORMAT, 0, (LPARAM) &pf); SendMessage(hmsg, EM_REPLACESEL, FALSE, (LPARAM) from); SendMessage(hmsg, EM_REPLACESEL, FALSE, (LPARAM) L"\n"); } pf.wAlignment = align[0]; SendMessage(hmsg, EM_SETPARAFORMAT, 0, (LPARAM) &pf); if (title && wcslen(title)) { /* Increase font size and set font color for title of the message */ SendMessage(hmsg, EM_GETCHARFORMAT, SCF_DEFAULT, (LPARAM) &cfm); cfm.dwMask |= CFM_SIZE|CFM_COLOR; cfm.yHeight = MulDiv(cfm.yHeight, 4, 3); /* scale up by 1.33: 12 pt if default is 9 pt */ cfm.crTextColor = RGB(0, 0x33, 0x99); cfm.dwEffects = 0; SendMessage(hmsg, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) &cfm); SendMessage(hmsg, EM_REPLACESEL, FALSE, (LPARAM) title); SendMessage(hmsg, EM_REPLACESEL, FALSE, (LPARAM) L"\n"); } /* Revert to default font and set the text */ SendMessage(hmsg, EM_GETCHARFORMAT, SCF_DEFAULT, (LPARAM) &cfm); SendMessage(hmsg, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) &cfm); if (text) { SendMessage(hmsg, EM_REPLACESEL, FALSE, (LPARAM) text); SendMessage(hmsg, EM_REPLACESEL, FALSE, (LPARAM) L"\n"); } /* revert alignment */ pf.wAlignment = pf_align_saved; SendMessage(hmsg, EM_SETPARAFORMAT, 0, (LPARAM) &pf); /* Remove lines from the window if it is getting full * We allow the window to grow by upto 50 lines beyond a * max value before truncating */ int pos2 = SendMessage(hmsg, EM_GETLINECOUNT, 0, 0); if (pos2 > MAX_MSG_LINES + 50) { int pos1 = SendMessage(hmsg, EM_LINEINDEX, MAX_MSG_LINES, 0); SendMessage(hmsg, EM_SETSEL, pos1, -1); SendMessage(hmsg, EM_REPLACESEL, FALSE, (LPARAM) _T("")); PrintDebug(L"Text from character position %d to end removed", pos1); } /* Select top of the message and scroll to there */ SendMessage(hmsg, EM_SETSEL, 0, 0); SendMessage(hmsg, EM_SCROLLCARET, 0, 0); } /* A modeless message box. * Use AddMessageBoxText to add content and display * the window. On WM_CLOSE the window is hidden, not destroyed. */ static INT_PTR CALLBACK MessageDialogFunc(HWND hwnd, UINT msg, UNUSED WPARAM wParam, LPARAM lParam) { HICON hIcon; HWND hmsg; const UINT top_margin = DPI_SCALE(16); const UINT side_margin = DPI_SCALE(20); NMHDR *nmh; switch (msg) { case WM_INITDIALOG: hIcon = LoadLocalizedIcon(ID_ICO_APP); if (hIcon) { SendMessage(hwnd, WM_SETICON, (WPARAM) (ICON_SMALL), (LPARAM) (hIcon)); SendMessage(hwnd, WM_SETICON, (WPARAM) (ICON_BIG), (LPARAM) (hIcon)); } hmsg = GetDlgItem(hwnd, ID_TXT_MESSAGE); SetWindowText(hwnd, L"OpenVPN Messages"); SendMessage(hmsg, EM_SETMARGINS, EC_LEFTMARGIN|EC_RIGHTMARGIN, MAKELPARAM(side_margin, side_margin)); if (LangFlowDirection() == 1) { LONG exstyle = GetWindowLong(hwnd, GWL_EXSTYLE); SetWindowLong(hwnd, GWL_EXSTYLE, exstyle | WS_EX_RTLREADING | WS_EX_LAYOUTRTL); exstyle = GetWindowLong(hmsg, GWL_EXSTYLE); SetWindowLong(hmsg, GWL_EXSTYLE, exstyle | WS_EX_LEFTSCROLLBAR); } enable_url_detection(hmsg); /* Position the window close to top right corner of the screen */ RECT rc; GetWindowRect(hwnd, &rc); OffsetRect(&rc, -rc.left, -rc.top); int ox = GetSystemMetrics(SM_CXSCREEN); /* screen size along x */ ox -= rc.right + DPI_SCALE(rand()%50 + 25); int oy = DPI_SCALE(rand()%50 + 25); SetWindowPos(hwnd, HWND_TOP, ox > 0 ? ox : 0, oy, 0, 0, SWP_NOSIZE); return TRUE; case WM_SIZE: hmsg = GetDlgItem(hwnd, ID_TXT_MESSAGE); /* leave some space as top margin */ SetWindowPos(hmsg, NULL, 0, top_margin, LOWORD(lParam), HIWORD(lParam)-top_margin, 0); InvalidateRect(hwnd, NULL, TRUE); break; /* set the whole client area background to white */ case WM_CTLCOLORDLG: case WM_CTLCOLORSTATIC: return (INT_PTR) GetStockObject(WHITE_BRUSH); break; case WM_COMMAND: if (LOWORD(wParam) == ID_TXT_MESSAGE) { /* The caret is distracting in a readonly msg box: hide it when we get focus */ if (HIWORD(wParam) == EN_SETFOCUS) { HideCaret((HWND)lParam); } else if (HIWORD(wParam) == EN_KILLFOCUS) { ShowCaret((HWND)lParam); } } break; /* Must be sent with lParam = connection pointer * Adds the current echo message and shows the window. */ case WM_OVPN_ECHOMSG: { connection_t *c = (connection_t *) lParam; wchar_t from[256]; _sntprintf_0(from, L"From: %ls %ls", c->config_name, _wctime(&c->echo_msg.fp.timestamp)); /* strip \n added by _wctime */ if (wcslen(from) > 0) { from[wcslen(from)-1] = L'\0'; } AddMessageBoxText(hwnd, c->echo_msg.text, c->echo_msg.title, from); SetForegroundWindow(hwnd); ShowWindow(hwnd, SW_SHOW); } break; case WM_NOTIFY: nmh = (NMHDR *) lParam; /* We handle only EN_LINK messages */ if (nmh->idFrom == ID_TXT_MESSAGE && nmh->code == EN_LINK) { return OnEnLinkNotify(hwnd, (ENLINK *)lParam); } break; case WM_CLOSE: ShowWindow(hwnd, SW_HIDE); return TRUE; } return 0; }