Использование технологии Microsoft Active Accessibility для доступа к содержимому браузера

Давайте придумаем решение вот такой-вот простенькой задачи.Имеется: браузер (IE, Chrome или Firefox), уже запущенный пользователем.Требуется: написать программу, которая получит URL, который в данный момент введён в адресной строке.Давайте подумаем, каким образом эту простенькую задачу решить НЕ получится:

1. FindWindow + GetWindowText

Почему не получится Первая идея — найти окно браузера, в нём дочернее окно адресной строки и взять URL оттуда. Практика показывает, что отдельное дочернее окно для адресной строки имеет только IE. FF и Chrome кросплатформенны, поэтому предпочитают весь свой контент отрисовывать самостоятельно.

2. Браузерное расширение, которое отдаст URL нашей программе (например, через запрос к localhost)Почему не получится Можно. Но во-первых, для трёх браузеров нужно будет написать 3 разных расширения, а во-вторых, для FF и Chrome мы будем вынуждены распространять его только через их магазины расширений. Писать программу, работоспособность которой зависит от того, зачешется ли сегодня левая пятка модератора — нет уж, увольте.

3. Давайте напишем сниффер и посмотрим что там пользователь открывалПочему не получится А давайте! Но что дальше? Даже если из потока трафика мы выделим данные, полученные именно браузером и расшифруем HTTP-протокол, мы всё-равно не узнаем именно текущий URL (ссылок в потоке будет много). Кроме того, сразу идут в сад HTTPS-соединения, HTTP/2, ссылки на локально открытые файлы, ссылки на внутренние страницы (типа chrome://settings) и т.д. 4. Давайте воспользуемся Remote Debugging Protocol ну или каким-нибудь Selenium-омПочему не получится Не подходит из-за ограничения условий исходной задачи: браузер уже запущен, мы не можем запустить новый подконтрольный экземпляр, нам нужно взаимодействовать с уже имеющимся.

5. Может быть, хуки? Почему не получится Ну, внедриться-то мы в браузер сможем. А на что вешать хуки? Для IE всё ясно — SetWindowText для окна адресной строки (но с ним и более простой способ №1 проходил). А в FF и Chrome у нас нет каких-то чётко определённых объектов и интерфейсов, на которые мы можем завязаться. Можно что-то сделать с конкретной версией браузера, но универсального решения не получится.

6. Скриншот окна браузера, определение положения адресной строки, распознавание текста с картинки! Почему не получится Уже как-то начинает смахивать на отчаяние, правда? Прикинем все варианты цветовых схем ОС, разрешений, масштабов, учтём наличие в браузере плагинов, цветовых схем, нестандартного расположения элементов, right-to-left языковых локалей ну и закончим случаем, когда окно адресной строки слишком узкое, чтобы вместить URL полностью.

7. Ваш вариантА напишите в комментариях, какие ещё решения вам приходят в голову и мы подумаем, получится или нет.А теперь один из правильных ответов: мы воспользуемся уже старенькой, но весьма стабильной и поддерживаемой всеми браузерами во всех ОС с Win95 до Win10 технологией Microsoft Active Accessibility, которая даст нам возможность не только получить текущий URL (при чём одинаковым образом для всех браузеров), но и вообще дать доступ ко всему контенту браузера — от самого родительского окна с его заголовком, меню, тулбаром, вкладками и до содержимого открытой веб-страницы вплоть до самого последнего её элемента.

Введение Microsoft Active Accessibility (MSAA) придумали аж в 1997-ом году и сделали её для того, чтобы стало возможным писать экранные лупы, приложения для чтения текста с экрана и создания прочих программ, улучшающим взаимодействие с компьютером людей с ограниченными физическими возможностями (проблемы со зрением, слухом и т.д.). Поддержка технологии в IE появилась давно, в FF и Chrome тоже была добавлена чуть позже. С выходом Vista появилось улучшение — Windows Automation API, однако и старый добрый MSAA никуда не делся, отлично работает с последними ОС и браузерами.Код В общем, ничего сложного в коде нет. Входной точкой для нас будет родительское окно браузера, которое можно получить по его ClassID: FindWindow (L«IEFrame», NULL); // IE FindWindow (L«MozillaWindowClass», NULL); // Firefox FindWindow (L«Chrome_WidgetWin_1», NULL); // Chrome. Этот код может сработать, но вообще-то документация (http://www.chromium.org/developers/design-documents/accessibility) рекомендует перебирать все окна, класс которых начинающиеся с «Chrome», на случай, если им взбредёт в голову изменить название класса. Из практики можно добавить, что перебирать нужно окна с таким class name и непустым заголовком. Дальше нужно у этого окна получить указатель на COM-интерфейс IAccessible

:: AccessibleObjectFromWindow (hWndChrome, OBJID_CLIENT, IID_IAccessible, (void**)(&pAccMain)); Да, перед этим не забудьте:

Подключить заголовочный файл #include «oleacc.h» Прилинковать Oleacc.lib Инициализировать COM вызовом функции :: CoInitialize (NULL); Это очень важно не забыть! Без этого у вас что-то может начать работать, но в непредвиденные моменты вы получите странные ошибки. Также возможна ситуация, когда никаких ошибок не будет, но вы просто не получите часть данных. В общем, очень подлая и совершенное не поддающаяся отладке ошибка. Итак, у нас есть указатель на IAccessible. Что это такое? Это корневой узел дерева, описывающего весь браузер — окно, заголовок, меню, тулбары, адресную строку, контент страницы, статусбар. Как бы это всё увидеть в наглядном виде? Нет ничего проще! Microsoft для этого предоставляет утилиту inspect.exe (поставляется с Windows SDK, у меня лежит в папке C:\Program Files (x86)\Windows Kits\8.0\bin\x64). Разработчики Хромиума рекомендуют утилиту aViewer.

Давайте посмотрим, как выглядят деревья доступных элементов браузеров: IEbd2b2230a17b45808882d33f464649d5.png

Chrome967fbde7c5f844fbbd7e1e4fdf90c8b7.png

Firefox6f56c8b49f5f4b54b6d6e743c3fa5502.png

Как мы видим, адресная строка доступна через интерфейс IAccessible во всех браузерах. Названия элементов, положение в дереве в разных браузерах разное, но в общем-то для доступа к адресной строке нам нужно только пара функций: возможность получения имени и значения текущего элемента и возможность получения детей текущего элемента дерева.

И то и другое пишется просто, вот финальный код, получающий текущий URL для Chrome.

#include «stdafx.h» #include #include #include «windows.h» #include «oleacc.h» #include «atlbase.h»

std: wstring GetName (IAccessible *pAcc) { CComBSTR bstrName; if (! pAcc || FAILED (pAcc→get_accName (CComVariant ((int)CHILDID_SELF), &bstrName)) || ! bstrName.m_str) return L»; return bstrName.m_str; }

HRESULT WalkTreeWithAccessibleChildren (CComPtr pAcc) { long childCount = 0; long returnCount = 0;

HRESULT hr = pAcc→get_accChildCount (&childCount);

if (childCount == 0) return S_OK;

CComVariant* pArray = new CComVariant[childCount]; hr = :: AccessibleChildren (pAcc, 0L, childCount, pArray, &returnCount); if (FAILED (hr)) return hr;

for (int x = 0; x < returnCount; x++) { CComVariant vtChild = pArray[x]; if (vtChild.vt != VT_DISPATCH) continue; CComPtr pDisp = vtChild.pdispVal; CComQIPtr pAccChild = pDisp; if (! pAccChild) continue;

std: wstring name = GetName (pAccChild).data (); if (name.find (L«Адресная строка и строка поиска») != -1) { CComBSTR bstrValue; if (SUCCEEDED (pAccChild→get_accValue (CComVariant ((int)CHILDID_SELF), &bstrValue)) && bstrValue.m_str) std: wcout << std::wstring(bstrValue.m_str).c_str();

return S_FALSE; }

if (WalkTreeWithAccessibleChildren (pAccChild) == S_FALSE) return S_FALSE; }

delete[] pArray; return S_OK; }

HWND hWndChrome = NULL;

BOOL CALLBACK FindChromeWindowProc (HWND hwnd, LPARAM lParam) { wchar_t className[100]; if (GetClassName (hwnd, className, 100) == 0 || wcscmp (className, L«Chrome_WidgetWin_1») != 0) return TRUE;

wchar_t title[1000]; if (GetWindowText (hwnd, title, 1000) == 0 || wcslen (title) == 0) return TRUE; hWndChrome = hwnd; return FALSE; }

int _tmain (int argc, _TCHAR* argv[]) { :: CoInitialize (NULL); EnumWindows (FindChromeWindowProc, 0);

if (hWndChrome == NULL) return 0;

CComPtr pAccMain; HRESULT hr = :: AccessibleObjectFromWindow (hWndChrome, 1, IID_IAccessible, (void**)(&pAccMain)); // 1 — захардкоженный идентификатор ловушки

CComPtr pAccMain2; :: AccessibleObjectFromWindow (hWndChrome, OBJID_CLIENT, IID_IAccessible, (void**)(&pAccMain2));

WalkTreeWithAccessibleChildren (pAccMain2);

return 0; } Результат работы:

fd0b35aced914c40920aaf4657f196fc.png

Для остальных браузеров всё аналогично.

Мелкий нюанс Технология MSAA в Chrome по-умолчанию отключена. Это связано с архитектурой Хрома: его разделение на процессы приводит к тому, что ни в каком одном процессе нет информации обо всём дереве элементов, необходимых MSAA. Разработчики Хрома не дураки и предусмотрели включение сбора этой информации и её кеширование в главном процессе. Но поскольку это всё несколько ресурсозатратно, а технология MSAA нужна относительно небольшому количеству людей — они её по-умолчанию выключили. Включить её можно двумя способами: Ручной: пойти в Хроме по ссылке chrome://accessibility и включить Программный: Хром создаёт специальную «ловушку», которой можно послать сообщение о том, что в системе присутствует приложение, использующее MSAA. Отправить в эту ловушку сообщение можно вот так: CComPtr pAccMain; HRESULT hr = :: AccessibleObjectFromWindow (hwnd, 1, IID_IAccessible, (void**)(&pAccMain)); // hwnd — главное окно Хрома, 1 — захардкоженный идентификатор ловушки

© Habrahabr.ru