...

понедельник, 23 марта 2015 г.

Использование технологии 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://ift.tt/18SwqVJ) рекомендует перебирать все окна, класс которых начинающиеся с "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.


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

IE


Chrome


Firefox


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


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



#include "stdafx.h"
#include <string>
#include <iostream>
#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<IAccessible> 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<IDispatch> pDisp = vtChild.pdispVal;
CComQIPtr<IAccessible> 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<IAccessible> pAccMain;
HRESULT hr = ::AccessibleObjectFromWindow(hWndChrome, 1, IID_IAccessible, (void**)(&pAccMain)); // 1 - захардкоженный идентификатор ловушки

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

WalkTreeWithAccessibleChildren(pAccMain2);

return 0;
}


Результат работы:



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


Мелкий нюанс




Технология MSAA в Chrome по-умолчанию отключена. Это связано с архитектурой Хрома: его разделение на процессы приводит к тому, что ни в каком одном процессе нет информации обо всём дереве элементов, необходимых MSAA. Разработчики Хрома не дураки и предусмотрели включение сбора этой информации и её кеширование в главном процессе. Но поскольку это всё несколько ресурсозатратно, а технология MSAA нужна относительно небольшому количеству людей — они её по-умолчанию выключили. Включить её можно двумя способами:


  • Ручной: пойти в Хроме по ссылке chrome://accessibility и включить

  • Программный: Хром создаёт специальную «ловушку», которой можно послать сообщение о том, что в системе присутствует приложение, использующее MSAA. Отправить в эту ловушку сообщение можно вот так:

    CComPtr<IAccessible> pAccMain;
    HRESULT hr = ::AccessibleObjectFromWindow(hwnd, 1, IID_IAccessible, (void**)(&pAccMain)); // hwnd - главное окно Хрома, 1 - захардкоженный идентификатор ловушки





This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий