Last active
October 31, 2022 03:23
-
-
Save anishsane/43b72f4fa117dea389b4e971bf6cbc1c to your computer and use it in GitHub Desktop.
A wrapper over the vlaci/openconnect-sso for windows
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Usage: | |
python3.exe openconnect-sso-wrapper.py "<hover-text>" "<server>" "<email>" | |
e.g. | |
python3.exe openconnect-sso-wrapper.py "OpenConnect-sso vpn01" "https://vpn01.company.com/SAML" "ceo.himself@company.com" | |
Prerequisites: | |
It needs 3 icon files in working directory corresponding to below states: | |
Disconnected.ico - VPN disconnected | |
Connecting.ico - VPN connecting/disconnecting | |
Connected.ico - VPN connected | |
I chose these on my system: | |
Connected.ico - https://iconarchive.com/show/origami-colored-pencil-icons-by-double-j-design/green-lock-icon.html | |
Connecting.ico - https://iconarchive.com/show/origami-colored-pencil-icons-by-double-j-design/blue-lock-icon.html | |
Disconnected.ico - https://iconarchive.com/show/origami-colored-pencil-icons-by-double-j-design/yellow-unlock-icon.html | |
You can easily edit the script to use 4 icons if you want separate ones for connecting/disconnecting. | |
Notes: | |
1. I start the process in administrator mode if it was not already started as admin. This is to avoid the repeated UAC prompts when openconnect-sso.exe tries to launch openconnect.exe with sudo. | |
If you don't want it, you can comment lines 16-20. | |
2. It will be great if we could use py2exe and generate a single exe containing this py script and the icon files. I have not explored that option yet. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# Module : SysTrayIcon.py | |
# Synopsis : Windows System tray icon. | |
# Programmer : Simon Brunning - simon@brunningonline.net | |
# Date : 11 April 2005 | |
# Notes : Based on (i.e. ripped off from) Mark Hammond's | |
# win32gui_taskbar.py and win32gui_menu.py demos from PyWin32 | |
import os | |
import sys | |
import win32com.shell.shell as shell | |
import ctypes | |
import threading | |
import time | |
if not ctypes.windll.shell32.IsUserAnAdmin(): | |
script = os.path.abspath(sys.argv[0]) | |
params = ' '.join([script] + sys.argv[1:]) | |
shell.ShellExecuteEx(lpVerb='runas', lpFile=sys.executable, lpParameters=params) | |
sys.exit(0) | |
import win32api | |
import win32con | |
import win32gui_struct | |
try: | |
import winxpgui as win32gui | |
except ImportError: | |
import win32gui | |
class SysTrayIcon(object): | |
QUIT = 'QUIT' | |
SPECIAL_ACTIONS = [QUIT] | |
FIRST_ID = 1023 | |
def __init__(self, | |
icon, | |
hover_text, | |
menu_options, | |
on_quit=None, | |
default_menu_index=None, | |
window_class_name=None,): | |
self.icon = icon | |
self.hover_text = hover_text | |
self.on_quit = on_quit | |
menu_options = menu_options + (('Disconnect and quit', None, self.QUIT),) | |
self._next_action_id = self.FIRST_ID | |
self.menu_actions_by_id = set() | |
self.menu_options = self._add_ids_to_menu_options(list(menu_options)) | |
self.menu_actions_by_id = dict(self.menu_actions_by_id) | |
del self._next_action_id | |
self.default_menu_index = (default_menu_index or 0) | |
self.window_class_name = window_class_name or "SysTrayIconPy" | |
message_map = {win32gui.RegisterWindowMessage("TaskbarCreated"): self.restart, | |
win32con.WM_DESTROY: self.destroy, | |
win32con.WM_COMMAND: self.command, | |
win32con.WM_USER+20 : self.notify,} | |
# Register the Window class. | |
window_class = win32gui.WNDCLASS() | |
hinst = window_class.hInstance = win32gui.GetModuleHandle(None) | |
window_class.lpszClassName = self.window_class_name | |
window_class.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW; | |
window_class.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW) | |
window_class.hbrBackground = win32con.COLOR_WINDOW | |
window_class.lpfnWndProc = message_map # could also specify a wndproc. | |
classAtom = win32gui.RegisterClass(window_class) | |
# Create the Window. | |
style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU | |
self.hwnd = win32gui.CreateWindow(classAtom, | |
self.window_class_name, | |
style, | |
0, | |
0, | |
win32con.CW_USEDEFAULT, | |
win32con.CW_USEDEFAULT, | |
0, | |
0, | |
hinst, | |
None) | |
win32gui.UpdateWindow(self.hwnd) | |
self.notify_id = None | |
self.refresh_icon(hover_text="The application has been minimized to system tray.", append_hover_text=False) | |
win32gui.PumpMessages() | |
def _add_ids_to_menu_options(self, menu_options): | |
result = [] | |
for menu_option in menu_options: | |
option_text, option_icon, option_action = menu_option | |
if callable(option_action) or option_action in self.SPECIAL_ACTIONS: | |
self.menu_actions_by_id.add((self._next_action_id, option_action)) | |
result.append(menu_option + (self._next_action_id,)) | |
elif non_string_iterable(option_action): | |
result.append((option_text, | |
option_icon, | |
self._add_ids_to_menu_options(option_action), | |
self._next_action_id)) | |
else: | |
print('Unknown item', option_text, option_icon, option_action) | |
self._next_action_id += 1 | |
return result | |
def refresh_icon(self, icon = None, hover_text = None, append_hover_text = True): | |
# Try and find a custom icon | |
hinst = win32gui.GetModuleHandle(None) | |
if icon: self.icon = icon | |
if hover_text: | |
notification_text = hover_text | |
if append_hover_text: hover_text = self.hover_text + ": " + hover_text | |
else: hover_text = self.hover_text | |
else: | |
notification_text = None | |
hover_text = self.hover_text | |
if os.path.isfile(self.icon): | |
icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE | |
hicon = win32gui.LoadImage(hinst, | |
self.icon, | |
win32con.IMAGE_ICON, | |
0, | |
0, | |
icon_flags) | |
else: | |
print("Can't find icon file - using default.") | |
hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) | |
if self.notify_id: message = win32gui.NIM_MODIFY | |
else: message = win32gui.NIM_ADD | |
self.notify_id = (self.hwnd, | |
0, | |
win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, | |
win32con.WM_USER+20, | |
hicon, | |
hover_text) | |
win32gui.Shell_NotifyIcon(message, self.notify_id) | |
if notification_text: | |
win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, | |
(self.hwnd, 0, | |
win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP | win32gui.NIF_INFO, | |
win32con.WM_USER+20, | |
hicon, | |
hover_text, | |
notification_text, 200, self.hover_text)) | |
def restart(self, hwnd, msg, wparam, lparam): | |
self.refresh_icon() | |
def destroy(self, hwnd, msg, wparam, lparam): | |
if self.on_quit: self.on_quit(self) | |
nid = (self.hwnd, 0) | |
win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid) | |
win32gui.PostQuitMessage(0) # Terminate the app. | |
def notify(self, hwnd, msg, wparam, lparam): | |
if lparam==win32con.WM_LBUTTONDBLCLK: | |
self.execute_menu_option(self.default_menu_index + self.FIRST_ID) | |
elif lparam==win32con.WM_RBUTTONUP: | |
self.show_menu() | |
elif lparam==win32con.WM_LBUTTONUP: | |
pass | |
return True | |
def show_menu(self): | |
menu = win32gui.CreatePopupMenu() | |
self.create_menu(menu, self.menu_options) | |
#win32gui.SetMenuDefaultItem(menu, 1000, 0) | |
pos = win32gui.GetCursorPos() | |
# See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/menus_0hdi.asp | |
win32gui.SetForegroundWindow(self.hwnd) | |
win32gui.TrackPopupMenu(menu, | |
win32con.TPM_LEFTALIGN, | |
pos[0], | |
pos[1], | |
0, | |
self.hwnd, | |
None) | |
win32gui.PostMessage(self.hwnd, win32con.WM_NULL, 0, 0) | |
def create_menu(self, menu, menu_options): | |
for option_text, option_icon, option_action, option_id in menu_options[::-1]: | |
if option_icon: | |
option_icon = self.prep_menu_icon(option_icon) | |
if option_id in self.menu_actions_by_id: | |
item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text, | |
hbmpItem=option_icon, | |
wID=option_id) | |
win32gui.InsertMenuItem(menu, 0, 1, item) | |
else: | |
submenu = win32gui.CreatePopupMenu() | |
self.create_menu(submenu, option_action) | |
item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text, | |
hbmpItem=option_icon, | |
hSubMenu=submenu) | |
win32gui.InsertMenuItem(menu, 0, 1, item) | |
def prep_menu_icon(self, icon): | |
# First load the icon. | |
ico_x = win32api.GetSystemMetrics(win32con.SM_CXSMICON) | |
ico_y = win32api.GetSystemMetrics(win32con.SM_CYSMICON) | |
hicon = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON, ico_x, ico_y, win32con.LR_LOADFROMFILE) | |
hdcBitmap = win32gui.CreateCompatibleDC(0) | |
hdcScreen = win32gui.GetDC(0) | |
hbm = win32gui.CreateCompatibleBitmap(hdcScreen, ico_x, ico_y) | |
hbmOld = win32gui.SelectObject(hdcBitmap, hbm) | |
# Fill the background. | |
brush = win32gui.GetSysColorBrush(win32con.COLOR_MENU) | |
win32gui.FillRect(hdcBitmap, (0, 0, 16, 16), brush) | |
# unclear if brush needs to be feed. Best clue I can find is: | |
# "GetSysColorBrush returns a cached brush instead of allocating a new | |
# one." - implies no DeleteObject | |
# draw the icon | |
win32gui.DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, win32con.DI_NORMAL) | |
win32gui.SelectObject(hdcBitmap, hbmOld) | |
win32gui.DeleteDC(hdcBitmap) | |
return hbm | |
def command(self, hwnd, msg, wparam, lparam): | |
id = win32gui.LOWORD(wparam) | |
self.execute_menu_option(id) | |
def execute_menu_option(self, id): | |
menu_action = self.menu_actions_by_id[id] | |
if menu_action == self.QUIT: | |
win32gui.DestroyWindow(self.hwnd) | |
else: | |
menu_action(self) | |
def non_string_iterable(obj): | |
try: | |
iter(obj) | |
except TypeError: | |
return False | |
else: | |
return not isinstance(obj, str) | |
if __name__ == '__main__': | |
import itertools, glob | |
import subprocess | |
openconnect = None | |
connected = False | |
hover_text = sys.argv[1] # Something like "OpenConnect-sso vpn01" | |
vpn_url = sys.argv[2] # Something like "https://vpn01.vpn.company.com/SAML" | |
email = sys.argv[3] # Something like "user@company.com" | |
def openconnect_sso(): | |
global openconnect | |
openconnect = subprocess.Popen(["openconnect-sso", "--server", vpn_url, "--user", email], stdout=subprocess.PIPE) | |
def monitorConnection(sysTrayIcon): | |
if openconnect is None: return | |
try: | |
while True: | |
line = openconnect.stdout.readline().decode('ascii') | |
if not line: | |
if not connected: return | |
sysTrayIcon.refresh_icon("Connecting.ico", "Re-connecting") | |
openconnect_sso() | |
time.sleep(5) | |
if 'Route configuration done' in line: | |
sysTrayIcon.refresh_icon("Connected.ico", "Connected") | |
openconnect.wait() | |
except: | |
sysTrayIcon.refresh_icon("Disconnected.ico", "Disconnected") | |
def connect(sysTrayIcon): | |
global connected | |
connected = True | |
if openconnect: return | |
sysTrayIcon.refresh_icon("Connecting.ico", "Connecting") | |
openconnect_sso() | |
threading.Thread(target = monitorConnection, args = (sysTrayIcon,), daemon = True).start() | |
def disconnect(sysTrayIcon): | |
global openconnect | |
global connected | |
connected = False | |
if openconnect is None: return | |
import signal | |
openconnect.send_signal(signal.CTRL_C_EVENT) | |
sysTrayIcon.refresh_icon("Connecting.ico", "Disconnecting") | |
try: openconnect.wait() | |
except: pass | |
openconnect = None | |
sysTrayIcon.refresh_icon("Disconnected.ico", "Disconnected") | |
menu_options = (('Connect', "Connected.ico", connect), | |
('Disconnect', "Disconnected.ico", disconnect), | |
) | |
SysTrayIcon("Disconnected.ico", hover_text, menu_options, on_quit=disconnect, default_menu_index=1) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment