Так сложилось, что у меня на работе используется Cisco AnyConnect Secure Mobility Client. Периодически приходится подключаться к рабочему VPN используя эту программу. Каждый раз необходимо вручную выполнять следующие шаги:
Нажатие кнопки "Connect".
Ввод постоянного пароля, затем нажатие кнопки "OK".
Ввод TOTP и нажатие кнопки "Continue".
Мне, как человеку постоянно работающему в терминале, захотелось иметь под рукой консольную утилиту, которая выполнит все описанные действия за меня сама. О существенной экономии времени речь конечно не идет. В то же время, надоело каждый раз смотреть из телефона TOPT, ожидая, пока не наступит время, за которое этот код не успеет протухнуть, пока я буду его вводить вручную. Ну и конечно было любопытно, а как можно программно взаимодействовать с разными GUI приложениями.
AppleScript
Для простоты сначала предположим, что у вас есть постоянный пароль и текущее значение TOTP в переменных окружения. Как программно сделать несколько кликов, а также заполнить соответствующие поля? После недолгих поисков в интернете было найдено решение: встроенный в MacOS скриптовый язык AppleScript, который позволяет управлять другими GUI приложениями. Вроде бы все очень просто, подумал я, имеющий почти 10-летний опыт программирования на различных языках. Но данный язык оказался плохо документированным, не консистентным и вызывающим толику фрустрации. Для того, чтобы сделать рабочую версию скрипта, у меня ушло около 3 часов времени. Чтобы писать на AppleScript есть встроенный Script Editor, его я и использовал для написания и отладки моей поделки.
Автоматизируем 1 шаг
На данном этапе необходимо просто активировать программу и нажать кнопку "Connect".
set appName to "Cisco AnyConnect Secure Mobility Client"
set vpnPwd to system attribute "VPN_PWD"
set vpnTotp to system attribute "VPN_TOTP"
tell application appName
activate
end tell
tell application "System Events" to tell process appName
repeat with appWindow in (every window whose subrole is not "AXUnknown")
perform action "AXRaise" of appWindow
end repeat
set cw to first window whose description is "standard window"
click button "Connect" of cw
end tell
Чтобы не писать длинное название программы каждый раз, сразу завел переменную appName. Постоянный пароль и TOTP, берем из переменных окружения VPN_PWD и VPN_TOTP соответственно. В строчках 5-7 активируем приложение. Однако, если оно уже активно и свернуто в Dock, то ничего не произойдет. Для того, чтобы в дальнейшем нажать кнопку, необходимо чтобы приложение было открыто. Поэтому в блоке начинающемся на строчке 9 будем воздействовать на на нужное окно (их на этом этапе - 2, потому что иконка в Tray считается отдельным окном и имеет subrole равный AXUnknown). Чтобы вытащить свернутое окошко из Dock, необходимо выполнить над ним действие AXRaise. И наконец, в 14 и 15 строчках находим нужное нам окно и кликаем кнопку с описанием "Connect".
Автоматизируем 2 шаг
Нужно ввести постоянный пароль из переменной окружения и нажать кнопку "OK".
...
set vpnPwd to system attribute "VPN_PWD"
...
tell application "System Events" to tell process appName
...
click button "Connect" of cw
set wcount to count (window)
repeat until wcount > 2
delay 0.2
set wcount to count (window)
end repeat
set cw to first window whose description is "dialog"
set value of text field 2 of cw to vpnPwd
click button "OK" of cw
end tell
На прошлом шаге мы нажали кнопку, но необходимо дождаться, пока появится следующее окно(3-е по счету). Ничего кроме классического check-wait-repeat цикла в 8-12 строчках я не придумал. Снова выбираем нужное окно, заполняем текстовое поле и кликаем кнопку.
Причуды с циклами
Кажущийся более простым, цикл ниже почему-то не работает в AppleScript, хотя именно его я попробовал вначале.
repeat until count (window) > 2
delay 0.2
end repeat
error "Can’t make window into type number, date or text." number -1700 from window to number, date or text
Автоматизируем 3 шаг
Необходимо ввести TOTP из переменной окружения и нажать заветную кнопку "Continue".
...
set vpnTotp to system attribute "VPN_TOTP"
...
tell application "System Events" to tell process appName
...
click button "OK" of cw
delay 0.3
set wcount to count (window)
repeat until wcount > 2
delay 0.2
set wcount to count (window)
end repeat
set cw to first window whose description is "dialog"
set value of text field 1 of cw to vpnTotp
click button "Continue" of cw
end tell
return
После нажатия кнопки на предыдущем шаге необходимо дождаться, пока пропадет старое окно и появится новое. Ждем 300ms и снова используем цикл из предыдущего шага. Затем повторяем уже описанные выше действия. Return с пустым значением нужен, чтобы последнее значение не попадало в stdout при запуске из терминала.
Полная версия скрипта - ciscovpn.applescript
set appName to "Cisco AnyConnect Secure Mobility Client"
set vpnPwd to system attribute "VPN_PWD"
set vpnTotp to system attribute "VPN_TOTP"
tell application appName
activate
end tell
tell application "System Events" to tell process appName
repeat with appWindow in (every window whose subrole is not "AXUnknown")
perform action "AXRaise" of appWindow
end repeat
set cw to first window whose description is "standard window"
click button "Connect" of cw
set wcount to count (window)
repeat until wcount > 2
delay 0.2
set wcount to count (window)
end repeat
set cw to first window whose description is "dialog"
set value of text field 2 of cw to vpnPwd
click button "OK" of cw
delay 0.3
set wcount to count (window)
repeat until wcount > 2
delay 0.2
set wcount to count (window)
end repeat
set cw to first window whose description is "dialog"
set value of text field 1 of cw to vpnTotp
click button "Continue" of cw
end tell
return
Получение TOTP
Для хранения токенов и получения TOTP из них я использую купленный когда-то Yubikey 5c nano(есть на Ozon и Яндекс маркет по цене в районе 7000 руб), далее будем говорить про него. Если у вас такого нет, можно использовать totp-cli c той лишь разницей, что токены он будет хранить на диске вашего компьютера. Для работы же с yubikey используется ykman. Вначале необходимо добавить наш токен. Обратите внимание на опцию --touch, она нужна для того, чтобы требовалось физическое касание ключа, во время получения TOTP.
ykman oath accounts add --touch <token_name> <token_value>
Далее TOTP можно получить следующей командой, при этом yubikey будет мигать показывая, что необходимо касание. Я держу его в боковом usb-c слоте своего монитора, что очень удобно.
ykman oath accounts code -s <token_name>
Заключение
Чтобы все заработало, нужно добавить в .zprofile(ну или другой файл, если вы предпочитаете отличный от zsh shell) строки ниже. После перезагрузки shell, можно будет подключиться одной командой connect и касанием yubikey. Ах да, последний штрих - добавить нужные разрешения безопасности для osascript(встроенный интерпретатор AppleScript).
export VPN_PWD=<static_password>
function connect() {
VPN_TOTP=$(ykman oath accounts code -s <token_name>) osascript ~/ciscovpn.applescript
}
P.S. Ну и куда без этого: подписывайтесь на мой телеграмм канал, где я галлюцинирую на разные темы, буду рад новым читателям.