Форум программистов, компьютерный форум, киберфорум
diadiavova
Войти
Регистрация
Восстановить пароль
Рейтинг: 3.67. Голосов: 3.

Управление браузером из внешнего приложения

Запись от diadiavova размещена 22.06.2020 в 09:00
Обновил(-а) diadiavova 06.08.2020 в 18:31

  1. Общие замечания
  2. Сервер
  3. Реализация расширения
  4. Приложение
  5. Запускаем – проверяем
Общие замечания

Если описывать задачу, которую мы здесь будем решать, в двух словах, то состоит она во взаимодействии настольного приложение с браузером. Мне известно о существовании Selenium WebDriver, однако, насколько я знаю, возможности его ограничиваются имитацией действий пользователя, доступом к DOM, управлением вкладками, окнами и тому подобными вещами. Мне же хотелось бы получить как можно больше полномочий, чтобы выполнять из внешнего приложения задачи, доступные только расширениям браузера. Вот, собственно, исследованием данного вопроса мы здесь и будем заниматься.
Сразу изложу идею. Поскольку изначально было заявлено, что действия, которые браузер должен будет выполнять под управлением внешней программы, то, соответственно помимо самой программы нам понадобится создать расширение для браузера. Далее нам нужно будет реализовать взаимодействие между расширением и приложением. На сегодняшний день эта задача решается предельно просто, а именно посредством веб-сокетов (WebSocket). То есть идея в том, чтобы в настольном приложении запустить сервер, принимающий подключения веб-сокетов, из расширения подключиться к этому серверу и далее через это подключение обмениваться командами и данными. Связь будет двусторонней, поэтому помимо задачи, обозначенной в заголовке, эту же модель взаимодействия можно использовать и для отправки данных приложению, и, если понадобится, то и управления приложением из расширения. Наша же основная задача состоит в том, чтобы отдать из приложения команду расширению и, если команда возвратит ответ, то получить этот ответ.

Сервер

Для создания сервера можно использовать два подхода. Во-первых, учитывая, что нам требуется минимальная функциональность, простейший сервер можно написать самостоятельно. Инструкция имеется здесь Writing a WebSocket server in C#. Во-вторых, существуют готовые решения, со всякими продвинутыми возможностями. Несмотря на то, что продвинутые возможности нам вряд ли понадобятся, лучше все-таки воспользоваться готовым решением, это сильно упростит процесс, ну и кроме того, некоторыми возможностями, может и есть смысл воспользоваться.
Непродолжительный поиск привел меня к следующему решению SuperWebSocket. Среди пакетов NuGet есть несколько от этого автора, которые реализуют веб-сокет-сервер. Я попробовал парочку, оба работают, поэтому, честно говоря, не знаю, какому отдать предпочтение. На самом деле это не так важно, учитывая, что нам не очень-то много от сервера и надо. Пакеты немного устарели, автор создал уже новую версию, но она не распространяется через NuGet, и насколько я понял, ориентирована только на .Net Core. Документация в проекте отсутствует, но автор также является автором проекта SuperSocket, который хорошо задокументирован и он пишет, что документация актуальна и для проекта SuperWebSocket тоже. Разница там только в том, какой сервер будет создан, а интерфейсы у них общие. Таким образом за документацией мы идем сюда SuperSocket 1.6 Documentation, нас интересует именно эта версия, поскольку последняя, которая 2.0 актуальна для той версии, которой в NuGet нет и мы ее использовать не будем.

Тестовое приложение

Для начала, чтобы уже сразу проверить сервер в работе, мы создадим простое приложение, код которого возьмем из документации с небольшими изменениями и попробуем его сконнектить с простой веб-страничкой.
Создаем консольное приложение. Добавляем пакеты NuGet
SuperSocket.WebSocket и SuperSocket.Engine
В результате помимо этих двух пакетов будет добавлен пакет SuperSocket. У меня все эти три пакета имеют версию 1.6.6.1
После установки SuperSocket.Engine в проекте появится папка Config с файлами log4net.config и log4net.unix.config. Эти файлы нужно выделить, перейти к свойствам и для свойства «Действие при сборке» выбрать «Содержание», а для свойства «Копировать в выходной каталог» выбрать значение «Копировать более позднюю версию»

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System;
using SuperSocket.WebSocket;
 
namespace SSTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var server = new WebSocketServer();
 
            Console.WriteLine("Press any key to start the server!");
            server.NewSessionConnected += Server_NewSessionConnected;
            server.NewMessageReceived += Server_NewMessageReceived;
 
            Console.ReadKey();
            Console.WriteLine();
 
            //Setup the appServer
            if (!server.Setup(212)) //Setup with listening port
            {
                Console.WriteLine("Failed to setup!");
                Console.ReadKey();
                return;
            }
 
            Console.WriteLine();
 
            //Try to start the appServer
            if (!server.Start())
            {
                Console.WriteLine("Failed to start!");
                Console.ReadKey();
                return;
            }
 
            Console.WriteLine("The server started successfully, press key 'q' to stop it!");
            char c;
            while ((c = Console.ReadKey().KeyChar) != 'q')
            {
                Console.WriteLine();
                continue;
            }
 
            //Stop the appServer
            server.Stop();
 
            Console.WriteLine("The server was stopped!");
            Console.ReadKey();
        }
 
        private static void Server_NewMessageReceived(WebSocketSession session, string value)
        {
            Console.WriteLine(value);
            session.Send($"Message received: <b>{value}</b>");
        }
 
        private static void Server_NewSessionConnected(WebSocketSession session)
        {
            session.Send("Welcome to SuperWebSocket Server");
        }
    }
 
}

Данный код запускает окно консоли и ждет нажатия любой клавиши, после которого запускает сервер. В примере из документации используется порт 2012, но у меня он не запускался на этом порте, видимо порт используется другой программой, поэтому я убрал из номера ноль и получился порт 212. Но это может быть любой свободный порт. При подключении к серверу, он отправляет сообщение с приглашением. Когда сервер получает текстовое сообщение он отправляет ответ о том, что получено сообщение такого-то содержания.
Веб-страничка выглядит так
PHP/HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
 
<body>
<input type="text" id="msg"/>
    <button id="btn1" onclick="socket.send(msg.value);">Btn1</button>
 
<div id="output"></div>
 
    <script>
        let socket = new WebSocket("ws://127.0.0.1:212");
function print(message) {
    document.getElementById("output").insertAdjacentHTML("beforeend", `<div>${message}</div>`);
}
socket.addEventListener("open", function (evt) {
   print("Connection started"); 
});
 
socket.addEventListener("message", function (evt) {
    print(evt.data); 
});
 
socket.addEventListener("close", function (evt) {
    print("Connection closed");
    
});
 
    </script>
</body>
 
</html>
Запускать ее можно как с локалхоста, так и из файловой системы, локальные подключение блокироваться не будут. По крайней мере из файрфокса не блокируются. Страничку надо запустить после того, как запущен сервер или обновить после этого. Там появится приветствие с сервера, можно также в текстовое поле что-то написать и нажать кнопку, сообщение будет отправлено и полученный ответ вставлен в страничку. На этом тестирование можно завершить.

О расширении для браузера


Это основная часть того, что мы будем делать. Расширение мы будем писать для Firefox, весь код я проверял только на Firefox Developer Edition. Выбор этого браузера обусловлен не только тем, что я именно им пользуюсь. Дело в том, что только у него я нашел возможность загрузить расширение из файла. Остальные браузеры позволяют делать это только в режиме разработки, что не очень удобно, хотя, если надо, то это тоже не проблема. Загружать подобное расширение в маркеты, смысла не вижу, поскольку оно будет «открывать все двери» и вряд ли пройдет проверку безопасности. Но даже если и загружать в маркеты, то, насколько я знаю, регистрация в них бесплатна только опять-таки у мозиллы, а платить за то, чтобы поставить на свой браузер свое же расширение – это, на мой взгляд, как-то странно. Собственно, поэтому файрфокс.
Для создания расширения в Visual Studio я создал проект node.js, можно создать любой проект JavaScript. В нем создал папку FirefoxExtension. Для того, чтобы включить поддержку IntelliSense для расширений попробовал подключать разные пакеты, но что-то дело не пошло. В конце концов нашел вот это. Это годится в основном для Chrome, но, поскольку API расширений сейчас разные браузеры поддерживают одни и те же, за исключением некоторых нюансов, то это вполне подойдет, хотя нюансы эти надо учитывать.
Опишу пару примеров отличий. Корневое пространство имен для доступа к API в браузере Chrome называется chrome, в то время как в браузерах Firefox и Edge оно называется browser. Насколько я помню, когда разбирался в этих вопросах впервые, я написал простейшее тестовое расширение для хрома, потом запустил его в файрфоксе и оно там заработало. Так что, по всей видимости в файрфоксе доступ к корневому объекту через chrome тоже поддерживается (сейчас проверять не буду, но насколько я помню – это так). Упомянутый проект chrome.intellisense поддерживает пространство имен chrome, поэтому для того, чтобы он поддерживал и browser, я просто в файле после объявления переменной chrome, объявил переменную browser и присвоил ей chrome. Таким образом эта проблема решилась. Другой важный момент: функции API расширений в основном асинхронные, но в хроме эта модель расширений поддерживается давно, а асинхронные функции появились в языке позже и таким образом асинхронность в них достигается за счет добавления в функции коллбэка, как дополнительного параметра. В файрфоксе же поддержку этой модели расширений реализовали относительно недавно и там функции возвращают реальные промайсы. Таким образом, при использовании chrome.intellisense данное обстоятельство просто придется иметь в виду. Можно использовать что-то другое, что-то типа webextensions-polyfill и т. п., но у меня в Visual Studio задействовать это не получилось, так что ничего по этому поводу сказать не могу.

Реализация расширения

Теперь о самом расширении. Нам нужно, чтобы пользователь имел возможность подключаться к заданному им же порту и отключаться от него, когда потребуется. Таким образом в папку расширения помимо обязательного файла manifest.json добавим файлы default_popup.html и default_popup.js. Это будет всплывающее окошко, которое будет появляться, когда пользователь клацнет по иконке расширения в браузере и скрипт для него. Кроме того, не будем забывать о том, что окошко popup существует только когда оно видимо, таким образом мы не можем разместить вебсокет в нем или его скрипте, поскольку при закрытии окошка будет закрываться и соединение, а нам нужно, чтобы оно работало постоянно. Таким образом нам понадобится еще фалй background.js.
В манифесте помимо чисто описательных пунктов нам нужно будет прописать наше всплывающее окошко, бэкграунд-скрипт, а также набор разрешений. По поводу разрешений тут, по всей видимости следовало бы разрешить все, ну по крайней мере все, к чему мы хотим иметь доступ. А поскольку мы хотим его иметь ко всему то и получается, что разрешить надо все.
Я взял для примера несколько разрешений, здесь не все, и пробовать из того, что есть, мы тоже будем не все, так что это просто пример и не более.
Это пока неполный код.
manifest.json
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
    "manifest_version": 2,
    "name": "Desktop interaction",
    "version": "1.0.0.0",
    "author": "diadiavova",
    "background": {
        "scripts": [
            "background.js"
        ]
    },
    "browser_action": {
        "default_popup": "default_popup.html"
    },
    "permissions": [
        "tabs",
        "<all_urls>",
        "activeTab",
        "storage",
        "webRequest",
        "downloads",
        "cookies",
        "notifications",
        "bookmarks"
    ]
}
На странице default_popup.html разместим поле для ввода номера порта, кнопку для сохранения номера порта, чтобы не вводить каждый раз, и кнопку подключения. Последняя – это кнопка-переключатель, код взял отсюда.
default_popup.html

PHP/HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<!DOCTYPE html>
 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
 
<head>
    <meta charset="utf-8" />
    <title></title>
    <style>
        body {
            width: 270px;
            height: 100px;
        }
 
        /* The switch - the box around the slider */
        .switch {
            position: relative;
            display: inline-block;
            width: 60px;
            height: 34px;
            left: 200px;
            top: 40px;
        }
 
        /* Hide default HTML checkbox */
        .switch input {
            display: none;
        }
 
        /* The slider */
        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            -webkit-transition: .4s;
            transition: .4s;
        }
 
        .slider:before {
            position: absolute;
            content: "";
            height: 26px;
            width: 26px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            -webkit-transition: .4s;
            transition: .4s;
        }
 
        input:checked+.slider {
            background-color: #2196F3;
        }
 
        input:focus+.slider {
            box-shadow: 0 0 1px #2196F3;
        }
 
        input:checked+.slider:before {
            -webkit-transform: translateX(26px);
            -ms-transform: translateX(26px);
            transform: translateX(26px);
        }
 
        /* Rounded sliders */
        .slider.round {
            border-radius: 34px;
        }
 
        .slider.round:before {
            border-radius: 50%;
        }
 
        .portcontainer {
            margin: 20px;
        }
    </style>
</head>
<body>
    <div id="portcontainer">
        <span>Port: </span><input type="number" min="1" max="65535" id="numPort" /><button
            id="btnSave">Сохранить</button>
    </div>
    <label class="switch">
        <input type="checkbox" id="connectionSwitch">
        <span class="slider round"></span>
    </label>
    <script src="default_popup.js"></script>
</body>
 
</html>
Теперь о скриптах. При переключении выключателя popup-окне у нас должно выполняться подключение или отключение в бэкграунд-скрипте. Таким образом скрипт этого окошка должен обмениваться сообщениями с background.js. Я обычно организовываю обмен сообщениями следующим образом:
Сообщение представляет из себя объект, в котором обязательно присутствует поле cmd, содержащее имя команды. На принимающей стороне объявляю переменную commands, представляющую из себя объект, у которого имена ключей совпадают с именами команд, передаваемых в сообщениях, а значениями этих ключей будут функции, принимающие и обрабатывающие сообщения.
Итак, со стороны окошка popup нам потребуется отправлять сообщения о включении и выключении переключателя, а также запрашивать состояние подключения, последнее нам потребуется делать при открытии окошка, чтобы установить переключатель во включенное состояние, если сокет соединен с сервером. Принимать же он должен только сообщение о закрытии соединения, чтобы переключатель установить в выключенное состояние, если соединение по каким-то причинам разорвалось пока окошко было активно (например приложение закрылось или остановило сервер.
default_popup.js

Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
document.getElementById("connectionSwitch").addEventListener("change", async function (evt)
{
    browser.runtime.sendMessage(
        evt.target.checked
            ? { "cmd": "connect", "port": +document.getElementById("numPort").value }
            : { "cmd": "disconnect" });
});
 
document.getElementById("btnSave").addEventListener("click", function (evt)
{
    let port = document.getElementById("numPort").value;
    browser.storage.local.set({ "lastPort": port });
});
 
let commands = {
    "turnOff": function (options)
    {
        let chb = document.getElementById("connectionSwitch");
        if (chb.checked) chb.checked = false;
    }
}
browser.runtime.onMessage.addListener(async function (message, sender, sendResponse)
{
    if (message.cmd in commands)
    {
        return await commands[message.cmd](message);
    }
});
 
browser.runtime.sendMessage({ "cmd": "connectionInfo" }).then(async function (data)
{
    let lastPort = (await browser.storage.local.get("lastPort")).lastPort;
    let numPort = document.getElementById("numPort");
    if (data.readyState == WebSocket.OPEN)
    {
        document.getElementById("connectionSwitch").checked = true;
        numPort.value = data.port;
    }
    else if (lastPort)
    {
        numPort.value = lastPort;
    }
});
Основная логика у нас будет реализована в background.js, поэтому о нем немного подробнее. Естественно нам нужно реализовать прием сообщений.
Javascript
1
2
3
4
5
6
7
8
browser.runtime.onMessage.addListener(async function (message, sender, sendResponse)
{
    if (message.cmd in commands)
    {
        let result = await commands[message.cmd](message);
        return result;
    }
});
Команды, которые мы будем принимать это connect, disconnect и connectionInfo. Создаем переменную socket.
Javascript
1
let socket;
Кроме того, нам нужно будет добавить две функции, для подписки на события сокета и отписки от них. Когда соединение будет разрываться, мы будем отписываться от всех событий и затирать ссылку на сокет, а при подключении будем создавать новый экземпляр и подписываться на его события.
Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function addListeners()
{
 
    socket.addEventListener("open", socket_open);
    socket.addEventListener("close", socket_close);
    socket.addEventListener("error", socket_error);
    socket.addEventListener("message", socket_message);
}
function removeListeners()
{
    socket.removeEventListener("open", socket_open);
    socket.removeEventListener("close", socket_close);
    socket.removeEventListener("error", socket_error);
    socket.removeEventListener("message", socket_message);
}
 
function socket_open(evt)
{
    console.log("Connection started");
}
function socket_close(evt)
{
    console.log("Connection closed");
    browser.runtime.sendMessage({ "cmd": "turnOff" });
}
function socket_error(evt)
{
    console.log(evt);
 
}
function socket_message(evt)
{
    console.log(evt.data);
    let msg = JSON.parse(evt.data);
    if (msg.cmd in appCommands)
    {
        appCommands[msg.cmd](msg);
    }
}
В обработчиках мы просто отправляем сообщение на консоль расширения. При разрыве соединения отправляем сообщение нашему окошку popup, а вот при получении сообщения от приложения как раз и выполняется основная работа. Здесь все реализовано так же, как и при обмене сообщениями, за исключением того, что от приложения мы не будем принимать JavaScript объект, а вместо этого мы получим строку. Таким образом целесообразно организовать обмен так, чтобы этой строкой был код JSON, который легко превратить в JavaScript объект. Получив этот код, мы его парсим и только после этого выполняем ту же операцию, что и при приеме сообщений.
Теперь осталось только реализовать команды. Для команд сообщений внутри расширения все довольно просто
Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let commands = {
    "connect": function (options)
    {
        this.disconnect();
        try
        {
            socket = new WebSocket(`ws://127.0.0.1:${options.port}`);
            addListeners();
            return { "success": true };
        }
        catch (e)
        {
            return { "success": false, "reason": e };
        }
    },
    "disconnect": function (options)
    {
        if (socket)
        {
            if (socket.readyState != WebSocket.CLOSED && socket.readyState != WebSocket.CLOSING)
            {
                removeListeners();
                socket.close();
            }
            socket = null;
        }
    },
    "connectionInfo": function (options)
    {
        if (socket)
        {
            return { "readyState": socket.readyState, "port": new URL(socket.url).port }
        }
        else return { "readyState": 0 }
    }
 
};
А вот что делать с командами, поступающими от приложения? Здесь проблема в том, какие именно команды нам нужны. Например можно создать команду, которая будет выполнять определенный JavaScript код на активной вкладке браузера.
Javascript
1
2
3
4
5
6
let appCommands = {
    "xscript": async function (msg)
    {
        socket.send(JSON.stringify(await browser.tabs.executeScript({ "code": msg.code })));
    }
}
Таким образом, если мы отправим расширению сообщение типа
JSON
1
{"cmd":"xscript", "code": "document.title"}
То оно должно будет вернуть приложению заголовок активной страницы. Но мы не можем предусмотреть таким образом все. В этой связи много команд лучше не реализовывать, поскольку это сильно усложнит использование расширения и вряд ли охватит все возможности. Лучше сделать наиболее часто используемые команды и главную команду eval, которая будет принимать код, который, в свою очередь, будет передан функции eval, затем исполнен и результат возвращен приложению.
Таким образом полный код бэкграунд-скрипта у нас будет следующим
background.js
Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
 * @type WebSocket
 * */
let socket;
 
function socket_open(evt)
{
    console.log("Connection started");
}
function socket_close(evt)
{
    console.log("Connection closed");
    browser.runtime.sendMessage({ "cmd": "turnOff" });
}
function socket_error(evt)
{
    console.log(evt);
 
}
function socket_message(evt)
{
    console.log(evt.data);
    let msg = JSON.parse(evt.data);
    if (msg.cmd in appCommands)
    {
        appCommands[msg.cmd](msg);
    }
}
 
let appCommands = {
    "eval": function (msg)
    {
        let result = eval(msg.code);
        if (result.constructor == Promise)
        {
            result.then(function (data)
            {
                socket.send(JSON.stringify(data));
            },
                function (err)
                {
                    socket.send(JSON.stringify({ "error": err }));
                });
        }
        else
        {
            socket.send(JSON.stringify(result));
        }
    },
    "xscript": async function (msg)
    {
        socket.send(JSON.stringify(await browser.tabs.executeScript({ "code": msg.code })));
    }
}
 
 
function addListeners()
{
 
    socket.addEventListener("open", socket_open);
    socket.addEventListener("close", socket_close);
    socket.addEventListener("error", socket_error);
    socket.addEventListener("message", socket_message);
}
function removeListeners()
{
    socket.removeEventListener("open", socket_open);
    socket.removeEventListener("close", socket_close);
    socket.removeEventListener("error", socket_error);
    socket.removeEventListener("message", socket_message);
}
 
function clearSocket()
{
    socket.close();
    removeListeners();
    socket = null;
}
 
 
 
 
let commands = {
    "connect": function (options)
    {
        this.disconnect();
        try
        {
            socket = new WebSocket(`ws://127.0.0.1:${options.port}`);
            addListeners();
            return { "success": true };
        }
        catch (e)
        {
            return { "success": false, "reason": e };
        }
    },
    "disconnect": function (options)
    {
        if (socket)
        {
            if (socket.readyState != WebSocket.CLOSED && socket.readyState != WebSocket.CLOSING)
            {
                removeListeners();
                socket.close();
            }
            socket = null;
        }
    },
    "connectionInfo": function (options)
    {
        if (socket)
        {
            return { "readyState": socket.readyState, "port": new URL(socket.url).port }
        }
        else return { "readyState": 0 }
    }
 
};
 
browser.runtime.onMessage.addListener(async function (message, sender, sendResponse)
{
    if (message.cmd in commands)
    {
        let result = await commands[message.cmd](message);
        return result;
    }
});

Если мы оставим все как есть, то при попытке выполнить команду eval мы получим сообщение системы безопасности, о том, что это запрещено. Чтобы разрешить выполнение этой функции в манифест нам нужно добавить ключ content_security_policy.
JSON
1
"content_security_policy": "default-src 'self';script-src 'unsafe-eval' 'self'; style-src 'self' 'unsafe-inline'; connect-src ws://127.0.0.1:*"
Здесь помимо скриптов мы еще установили правила для стилей и соединений. Для стилей это нужно из-за того, что обычно по умолчанию в расширениях разрешены встроенные в страницу стили, а поскольку у нас для default-src установлено значение ‘self’, то для неуказанных параметров применяться будет именно оно, стало быть его нужно добавить. Ну и для connect-src мы указали, что будем соединяться только с локалхостом через вебсокет. Само собой можно добавить, скажем wss://127.0.0.1:* или если мы собираемся использовать соединения с разными адресами, протоколами и т. д., то все тоже надо прописать. Но поскольку в данном случае все это вроде как не нужно, я думаю, того что есть будет достаточно.
С расширением закончили, перейдем к приложению.

Приложение

Приложение для тестов я написал на языке VB.Net, но его код настолько прост, что написание его не составить труда на любом языке. Главное мы уже сделали.
Создаем приложене WinForms, добавляем в него все те же пакеты NuGet, что и в консольном приложении, которое мы создали раенне. Так же поступаем с содержимым папки Config. После чего добавляем на форму Panel вверху, на которой мы разместим все для управления. А под ней SplitContainer с вертикальным расположением панелей. Панели контейнера зальем многострочными TextBox. Сверху tbMessageToSeng, снизу tbMessageReceived. В верхний будем вводить команду, в нижнем будут появляться ответы. На панели разместим NumericUpDowun nudPort для ввода имени порта, CheckBox с видом кнопки chbRunServer, для запуска сервера. ComboBox для выбора имени команды (у нас там будут имена eval и xscript) cbCommand. И Button btnSend для отправки сообщения.
Код формы
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Imports Newtonsoft.Json
Imports SuperSocket.SocketBase
Imports SuperWebSocket
 
Public Class Form1
 
    Dim WithEvents server As New WebSocketServer()
 
    Private Sub chbRunServer_CheckedChanged(sender As Object, e As EventArgs) Handles chbRunServer.CheckedChanged
        Dim chb As CheckBox = sender
        If chb.Checked Then
            chb.Text = "Остановить"
            If server.Setup(nudPort.Value) Then
                server.Start()
            Else
                chb.Checked = False
            End If
        Else
            chb.Text = "Запустить"
            server.Stop()
        End If
    End Sub
 
    Private Sub server_NewMessageReceived(session As WebSocketSession, value As String) Handles server.NewMessageReceived
        tbMessageReceived.Invoke(Sub() tbMessageReceived.AppendText(vbCrLf & value))
 
    End Sub
 
    Private Sub btnSend_Click(sender As Object, e As EventArgs) Handles btnSend.Click
        SendMsg()
    End Sub
 
    Private Sub SendMsg()
        Dim msg As New Dictionary(Of String, String)()
        msg.Add("cmd", cbCommand.Text)
        msg.Add("code", tbMessageToSend.Text)
 
        For Each ssn In server.GetAllSessions
            ssn.Send(JsonConvert.SerializeObject(msg))
        Next
    End Sub
End Class
Логика приложения довольно проста. При изменении состояния чекбокса выполняется запуск или остановка сервера, в соответствии с этим меняется текст чекбокса (запустить или остановить). У сервера обрабатывается событие получения нового сообщения, которое отправляется в tbMessageReceived, поскольку обработчик этого события выполняется в отдельном потоке, запись в текстовое поле производится через Invoke.
Для отправки сообщения мы сначала создаем словарик с полями cmd и code, который потом будет сериализован в JSON и отправлен расширению. А вот дальше идет цикл, в котором перебираются сессии сервера и всем отправляется одно и то же сообщение. Тут дело в том, что сервер, который мы использовали, рассчитан на многопользовательские подключения. Каждое подключение создает новую сессию. Мы не использовали несколько подключений, поэтому можем оправлять одно сообщение всем сессиям в количестве одна штука, но если нужно использовать несколько подключений, то само собой, придется как-то организовать работу с сессиями.

Запускаем – проверяем

Далее в FireFox открываем страничку about:debugging#/runtime/this-firefox, жмем «Загрузить временное дополнение», и выбираем наш файл manifest.json. Запускаем приложение. Вводим номер порта, например тот же 212, и жмем кнопку (она же чекбокс) «Запустить». После этого в браузере, где после загрузки расширения появился его значек на панели справа вверху(поскольку мы не добавляли значков, то будет отображаться стандартный файрфоксовский). Там появится наше окошко popup, в котором надо будет ввести тот же номер порта, что и в приложении, и подключиться с помощью кнопки подключения. Теперь все готово и можно приступать к тестированию функционала.
Для начала откроем в браузере какую-нибудь веб-страничку (страничка отладчика не подойдет, как и любая специальная вкладка). В приложении выберем в комбобоксе команду xscript и введем в верхнее текстовое поле
Javascript
1
document.title
Нажмем отправить и получим в нижем поле заголовок страницы активной вкладки браузера в виде JSON массива с единственной строкой.
Можно таким же образом изменить содержимое, скажем, главного заголовка страницы (если он там есть).
Javascript
1
document.querySelector("h1").textContent = "Новый заголовок";
Выполнив эту команду, мы увидим, что заголовок страницы изменился.
Мы можем сделать то же самое и с помощью команды eval, но тогда придется для запроса title ввести в текстовое поле команду
Javascript
1
browser.tabs.executeScript({"code","document.title"})
Но с помощью команды eval мы также можем сделать что-нибудь не связанное со страницей. Например, если мы захотим добавить на панель закладок букмарклет, который запускает окошко alert с заголовком страницы, то можно выполнить следующий код
Javascript
1
browser.bookmarks.create({ "title": "Show title", "parentId": "toolbar_____", "url": "javascript:(function(){alert(document.title)})();" })
После этого можно перейти к браузеру, и клацнуть по этому букмарклету и прочитать заголовок в алерте. А в поле ответа расширения мы увидим информацию о новой вкладке.
Вложения
Тип файла: zip BrowserInteraction.zip (9.70 Мб, 584 просмотров)
Размещено в Без категории
Показов 6915 Комментарии 14
Всего комментариев 14
Комментарии
  1. Старый комментарий
    Аватар для Avazart
    Цитата:
    Мне же хотелось бы получить как можно больше полномочий, чтобы выполнять из внешнего приложения задачи, доступные только расширениям браузера.
    Больше полномочий это про что? Что имеется виду Вам не позволяет делать Selenium ?
    Запись от Avazart размещена 01.11.2020 в 20:08 Avazart вне форума
  2. Старый комментарий
    Аватар для diadiavova
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    Больше полномочий это про что? Что имеется виду Вам не позволяет делать Selenium ?
    Так я же вроде бы все написал подробно. Речь идет о полном наборе возможностей, которые имеют расширения браузера. На самом деле я не в курсе, возможно селениум тоже это может, поскольку ознакомился с его функционалом очень обзорно, но по-моему там все довольно скромно. То есть там можно взаимодействовать с открытыми документами и т. п., но функционал самого браузера вроде как недоступен. Например та же работа с закладками, я приводил пример, в котором удаленно создал букмарклет и разместил его на панели закладок. Селениум такое может?
    Запись от diadiavova размещена 01.11.2020 в 22:43 diadiavova на форуме
  3. Старый комментарий
    Аватар для Avazart
    А зачем Вам панель закладок при автоматизации?
    Запись от Avazart размещена 01.11.2020 в 23:49 Avazart вне форума
    Обновил(-а) Avazart 01.11.2020 в 23:51
  4. Старый комментарий
    Аватар для diadiavova
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    А зачем Вам панель закладок при автоматизации?
    Так я вроде как об автоматизации нигде и не писал. Я с таким же успехом могу спросить для чего нужен доступ извне при автоматизации. Мне для этих целей юзерскриптов хватает, в крайнем случае можно расширение написать.
    В то же время есть некоторые задачи для которых возможностей того же расширения не хватает. Точнее не хватает полномочий. Данный подход, например позволяет без труда реализовать удобных редактор тех же букмарклетов. Размещаем букмарклет на панели, извлекаем код, сохраняем в файл, открываем этот фай в любом редакторе и отслеживаем изменения файла, которыми тут же обновляем букмарклет. Все можно редактировать код букмарклета в любимом редакторе и тут же переходить к браузеру и проверять в работе.
    Запись от diadiavova размещена 02.11.2020 в 00:08 diadiavova на форуме
  5. Старый комментарий
    Аватар для Avazart
    Справедливое замечание.

    Не по теме:

    Можете посоветовать какую-то книгу/руководство по созданию расширений для браузера?

    Запись от Avazart размещена 03.11.2020 в 15:40 Avazart вне форума
  6. Старый комментарий
    Аватар для diadiavova
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    Можете посоветовать какую-то книгу/руководство по созданию расширений для браузера?
    Если говорить об автоматизации вообще, то тут есть три направления:
    Во-первых, есть смысл обратить внимание на букмарклеты. У них есть свои ограничения, ну там по размеру, безопасности, да и редакторов для них нет, но для простых задач они подходят лучше всего. Не требуют установки расширений, работают в основных браузерах практически одинаково и, например, будучи обычными закладками, синхронизируются между устройствами.
    Во-вторых, можно использовать юзерскрипты. Это подразумевает установку одного расширения, но после этого можно легко написать скрипт под большинство задач автоматизации на страницах(там нет доступа к такому функционалу, как те же закладки, но со страницами можно делать много чего), если это освоить(а это на порядок проще написания собственного расширения), то в подавляющем большинстве случаев писать собственное расширение не понадобится.
    В-третьих, собственно написание расширения, но тут есть ряд моментов.

    Букмарклет - это обычная закладка, в которой адрес выглядит как-то так javascript:<you javascript code here>. Это довольно просто, подробности любой поисковик подскажет.
    Запись от diadiavova размещена 03.11.2020 в 16:52 diadiavova на форуме
    Обновил(-а) diadiavova 03.11.2020 в 16:54
  7. Старый комментарий
    Аватар для diadiavova
    Юзерскрипты были порождены старым расширением для файрфокса под названием GreaseMonkey. На данный момент оно уже не работает, но формат скриптов, разработанный для него, фактически стал стандартом для скриптов такого рода и на сегодняшний день есть несколько расширений, поддерживающих этот формат. Наиболее часто используется TamperMonkey, он есть для разных браузеров, но у меня как-то с ним не сложились отношения, он как-то попросил задонатить, я отказался и он перестал работать )). Мог бы нажать кнопочку, что, дескать, задоначу позже, он бы ждал, но периодически напоминал бы. Я вместо этого нашел Violentmonkey – Загрузите это расширение для Firefox (ru), поддерживает тот же формат скриптов, существует и для других браузеров Get Violentmonkey - Violentmonkey, проблем у меня с ним не было. Документация здесь Metadata Block - Violentmonkey.
    Запись от diadiavova размещена 03.11.2020 в 16:54 diadiavova на форуме
  8. Старый комментарий
    Аватар для diadiavova
    Ну и по поводу расширений. Насчет книг я подсказать ничего не могу. Однако документация по расширениям браузеров существует и она довольно подробная. На сегодняшний день основные браузеры поддерживают модель расширений, изначально созданную для хрома, однако везде есть свои нюансы, поэтому, хоть документация для одного браузера и может использоваться для написания расширений для другого, эти нюансы, все-таки, должны учитываться. Так что лучше курить родную документацию для каждого браузера.
    По хрому можно начать отсюда
    Getting Started Tutorial - Google Chrome
    По файрфоксу - отсюда Расширения браузера - Mozilla | MDN
    По edge отсюда Расширения Microsoft EDGE (Chromium) - Microsoft Edge Development | Microsoft Docs
    Запись от diadiavova размещена 03.11.2020 в 16:55 diadiavova на форуме
  9. Старый комментарий
    Аватар для diadiavova
    С оперой и сафари не работал, но думаю, найти тоже будет несложно.

    Кроме того в старых версия файрфокса поддерживались оверлей-расширения(они же xul-расширения), на сегодняшний день поддержка таких расширений сохранилась в форке файрфокса под названием PaleMoon. Документацию по ним еще пока можно найти на mdn, но как долго она там еще будет оставаться - неизвестно. Эти расширения хороши тем, что имеют полномочия практически те же, что и настольное приложение. Если это требуется, то есть смысл заглянуть и туда. Overlay extensions - Mozilla | MDN
    Но еще раз повторю, что в файрфоксе, начиная с 56-ой версии это уже не работает.
    Запись от diadiavova размещена 03.11.2020 в 16:56 diadiavova на форуме
  10. Старый комментарий
    Аватар для Avazart
    Я имел ввиду именно само расширение, а не код в каком-то там расширении.
    Про GreaseMonkey/TamperMonkey итак понятно, как я понимаю именно в них разработка уступает Selenium
    Запись от Avazart размещена 03.11.2020 в 17:01 Avazart вне форума
    Обновил(-а) Avazart 03.11.2020 в 17:03
  11. Старый комментарий
    Аватар для diadiavova
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    Я имел ввиду именно само расширение, а не код в каком-то там расширении.
    Про GreaseMonkey/TamperMonkey итак понятно, как я понимаю именно в них разработка уступает Selenium
    Я же все написал развернуто. Просто комментарии имеют ограничения по размеру, поэтому пришлось разбить, но есть там ссылки и на документацию по расширениям.
    Запись от diadiavova размещена 03.11.2020 в 17:22 diadiavova на форуме
  12. Старый комментарий
    Аватар для diadiavova
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    именно в них разработка уступает Selenium
    А в чем конкретно уступает?
    Запись от diadiavova размещена 03.11.2020 в 17:25 diadiavova на форуме
  13. Старый комментарий
    Аватар для Avazart
    Ну банально в логгировании? В сохранении данных в эксель, json прочие. И наборе библиотек.

    Документация все же не учебник/статья.
    Запись от Avazart размещена 03.11.2020 в 18:00 Avazart вне форума
  14. Старый комментарий
    Аватар для diadiavova
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    Ну банально в логгировании? В сохранении данных в эксель, json прочие. И наборе библиотек.
    Ну здесь обычные расширения мало чем смогут помочь, разве что упомянутые оверлей-расширения могут работать с файловой системой. Однако, даже если речь идет о юзерскриптах, то и там есть некоторые возможности. Вот например здесь у меня реализованы функции импорта и экспорта данных Userscripts для форума - О форуме - Ответ 14573649 - Киберфорум
    Кроме того, никто не мешает отправлять их на сервер. Я понимаю, что еще сервер надо запускать, но при работе с селениумом ведь тоже внешнее приложение нужно.
    Цитата:
    Сообщение от Avazart Просмотреть комментарий
    Документация все же не учебник/статья.
    Документация содержит не только справочник по апи, там очень много всего, включая статьи a la "начало работы" или "первое расширение".
    Запись от diadiavova размещена 03.11.2020 в 18:38 diadiavova на форуме
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru