
Привет! Я Игорь Кечайкин, руководитель группы разработки во Frontend-команде Flocktory. Недавно, решая задачу, связанную с API WebUSB для Fingerprint-атрибуции пользователя, задался совершенно не связанным теоретическим вопросом: а как создать с этим API драйверы на JavaScript?
Чтобы разобраться, изучил спецификацию API WebUSB, а теперь принёс на Хабр выжимку самых важных элементов. В статье я расскажу о принципах работы решения и том, что с ним делать веб-разработчику, который хочет напрямую реализовать на сайт функцию, например, 3D-принтинга с пользовательских девайсов. А также вы узнаете, насколько это безопасно — для сайта, внедряющего API, и для девайсов юзера.
Если интересно, как расширить возможности своих сайтов и избавиться от головной боли с адаптацией под всевозможные устройства, заходите в статью.
Полноценно стандарт WebUSB поддерживается только браузерами на ядре Chromium, поэтому все упоминания браузера будут подразумевать Chromium.
Что такое API WebUSB
API WebUSB — способ безопасного предоставления услуг USB-устройств в Интернете. Он предоставляет API, знакомый разработчикам, работавшими с существующими нативными USB-библиотеками, и раскрывает интерфейсы устройств, определенные существующими спецификациями. С помощью этого API производители оборудования получают возможность создавать кроссплатформенные JavaScript SDK для своих устройств. Вместо того, чтобы ждать, пока новый вид устройства будет достаточно популярен для браузеров, чтобы обеспечить определенный API, новое и инновационное оборудование можно строить с первого дня.
Мотивирующее прикладное ПО
Веб-драйверы
Компонуемость сети позволяет создать новую экосистему поддержки оборудования, полностью основанную на веб-технологиях. Возьмем в качестве примера 3D-принтеры. Представьте, что сайт, на котором размещены 3D-объекты, хочет интегрировать печать непосредственно на свою страницу. Сеть поддерживает 2D-печать, но для 3D-разновидности нет API. Если производители размещают встраиваемые страницы, которые используют API WebUSB для отправки данных на свои принтеры, сайты могут использовать эти страницы для интеграции поддержки оборудования.
Обновления и диагностика устройств
Хотя беспроводные протоколы, такие как Bluetooth, часто являются более удобным решением для потребительских устройств, порты USB продолжают использовать, так как это простое решение для подачи питания, а также оно может служить подключением последней инстанции, когда устройство не работает. Интегрируя инструменты обновления и диагностики на свой сайт поддержки, производитель оборудования может предоставлять инструменты для клиентов на любой платформе и собирать более качественные диагностические данные, когда клиент обращается за поддержкой через свой веб-сайт. Целевая страница позволяет производителю устройства направить пользователя в нужную часть своего веб-сайта для получения помощи по устройству.
Небольшая выжимка из спецификации API WebUSB дает понять, что ее авторы прямо предлагают использовать это API для создания кастомных драйверов на JavaScript, которые будут применяться браузером.
Что же, хорошо, четкая аргументация успешности в поставленном вопросе уже есть. При этом драйверы, еще и на JavaScript — заманчивое предложение, я продолжу свою теорию и попытаюсь узнать, в каких пределах оно допустимо.
Определимся с условиями
Поскольку понятие «драйвер» в разработке достаточно широкое, то договоримся, что именно мы под ним понимаем. Некую библиотеку, написанную на JavaScript с применением API WebUSB, которая позволяет осуществить взаимодействие вашей программы с подключаемым к компьютеру физическим устройством, например, веб-камерой, клавиатурой и прочим.
При этом под программой понимаем код, написанный на JavaScript, который исполняется в браузерном окружении, являясь частью браузерной объектной модели (BOM). И неважно, будет ли в последующем эта программа иметь какие-то визуальные элементы, затрагивая объектную модель документа (DOM).
При имеющихся вводных, получается следующая диаграмма:

Как видно, браузер пользователя выступает в роли виртуальной машины и выступает мостом взаимодействия физического устройства с вашей web-страницей. Где уровень взаимодействия с самим USB реализован через API WebUSB.
Можно сразу выделить большой плюс такой системы в виде кроссплатформенности для кастомного драйвера устройства, поскольку всю эту работу на себя берет браузер.
А это безопасно?
В этой системе уместно задаться резонным вопросом: «Не слишком ли это небезопасно — давать интернету доступ к периферии моего железа?». И вы будете не одиноки в подобном опасении. Например, разработчики Mozilla пометили данное API негативным флагом.
В свою очередь, разработчики API контраргументируют: в использование этого механизма добавлены условия, минимизирующие риск для безопасности. Рассмотрим список этих условий:
использование API возможно только после непосредственного согласия пользователя через диалоговое окно;

Диалоговое окно появляется после вызова метода запроса к соединению, например, в таком виде:
navigator.usb.requestDevice({ filters: [] })
.then((device) => this.usb_device = device)
.catch(e => console.log('can`t check USB deviсe', e))
.then(async () => await this.usb_device!.open())
.catch(e => console.log('can`t open USB deviсe', e));
Появление окна возможно только если само событие привязано к реальному действию пользователя, например, клику по кнопке или ссылке, т.е. просто из кода вызвать это диалоговое окно невозможно.
количество контекстов возможных типов устройств ограничено, либо для них существует отдельное API (например, WebHID)
Каждое устройство при подключении к хосту, сообщает какой класс USB-устройств оно представляет, полный перечень стандартизированных классов можно посмотреть здесь. В WebUSB же предусмотрен перечень именно защищенных, недоступных классов, т.е. тех, к которым у вас нет возможности подключиться (просто некоторые из них уже поддерживаются другими API).
Перечень защищенных (не доступных) устройств для API WebUSB:
Code | Description |
0x01 | Audio |
0x03 | HID (Human Interface Device) |
0x08 | Mass Storage |
0x0B | Smart Card |
0x0E | Video |
0x10 | Audio/Video Devices |
0xE0 | Wireless Controller |
Также, через WebUSB нельзя получить доступ к устройствам, не перечисленным в списке выше и, одновременно, инициализируемым на физическом уровне или на «железных» драйверах (например, к USB-хабу). Так как это устройство становится недоступным еще до создания окружения браузера, т.е. даже у последнего нет доступа к такому типу устройств.
Если вы, как и я, помимо документации иногда заглядываете в исходники непосредственной реализации, то вот вам ссылка на Mojo-описание в исходниках Chromium, откуда можно начинать «серфить» по определениям, вызовам, их контекстам и непосредственной реализации функциональности.
Интересный момент из истории коммитов. Для сериализации системных сообщений между USB-девайсом и хостом, в начальных реализациях API WebUSB разработчики Chromium подобное не делали «с нуля», а просто подключали как 3th-party библиотеку libusb.c. Т.е. они тащили эту библиотеку целиком, даже не обращаясь к возможному ее системному аналогу из операционной системы.
Совместно с другими 3th-party библиотеками и драйверами это позволило обеспечить кроссплатформенность без лишних манипуляций со стороны пользователя. А также показало, что любой пользователь браузера Chromium в чем-то немножко пользователь операционной системы ChromiumOS, поскольку браузер в таком случае уже самодостаточен и выступает почти как виртуальная машина.
одновременный доступ нескольких драйверов к одному устройству — невозможно;
Данное условие подразумевает, что если ваше устройство занято (имеет подключение) каким-то другим пользователем (сервисом, программой и прочим), причем в области операционной системы за пределами браузера, то при попытке взаимодействия с устройством вы получите ошибку соединения.
WebUSB работает в общем концепте взаимодействия операционной системы с периферией, не создавая условий, при котором может быть нарушена работа какого либо устройства из-за WebUSB.
Если ваше USB-устройство, например, поддерживающее CDC-Data (Communications Device Class), запущено операционной системой (вне браузера) и находится в моменте обмена сообщениями с хостом, то WebUSB нельзя вмешаться, нарушить и уж тем более модифицировать работу этого общения.
API доступно только в защищенных соединениях, т.е. только через HTTPS-соединения.
Это правило уже стало обыденностью в веб-разработке, в связи с чем не требует особого пояснения. Напомню, что исключение возможно для случаев работы веб-страницы на локальном веб-сервере с ip-адресом 127.0.0.1
или доменным именем localhost
. URL-ы с такими путями браузер воспринимает как защищенные соединения по умолчанию. Поэтому нет необходимости устанавливать сертификаты на локальную машину.
Что там за теория с драйвером
Вышеперечисленное отступление про обеспечение безопасности является очень важным. Можно сказать, оно является единственным условием, которое прочерчивает границу в понимании каждым из нас термина «драйвер».
А именно, изначально кажется, что драйвер устройства в традиционном понимании и драйвер при применении WebUSB не совместимы в равенстве. Поскольку, у последнего имеются ограничения в перечне интерфейсов, отличие в способе инициализации и зависимость от среды исполнения.
Но! Здесь же имеет место дуализм. Эти же определения также характеризуют обратное — именно из-за этих свойств написанный скрипт на JavaScript с применением API WebUSB отвечает условию считаться драйвером устройства в системе. Поскольку, если взглянуть на «классический» драйвер под другим углом, то он имеет совершенно идентичные свойства. Только с тем исключением, что в случае с WebUSB необходимо в качестве системы воспринимать Браузер как виртуальную машину вместо операционной системы, а также, что драйвер работоспособен, пока существует сеанс пользователя с доменной страницей.
Непонятно, давай проще!
Попробую пояснить. Чтобы ниже было проще сопоставить соответствие WebUSB-драйвера и классическое его понимание, выделим основные требования которыми должен обладать кастомный драйвер.
Драйвер должен инициализироваться в системе выполнения и уметь взаимодействовать с ней.
Здесь все просто. API WebUSB обладает всеми необходимыми методами для установления соединения с устройством, а также методами подписок на события изменением статуса взаимодействия с этим устройством.
То есть, WebUSB-драйвер отвечает этому условию.
Единственный спорный момент — в том, как инициализируется веб-драйвер. В случае применения API WebUSB его инициализация происходит «руками» пользователя, т.е. не автоматически, как мы привыкли с классическими драйверами.
Но и в таком случае есть «размытая альтернатива», когда применение такого драйвера осуществляется через браузерное расширение с описанием доступных устройств в файле manifest.json
. Хотя автор не рискует утверждать о работоспособности такого решения. И все же, для концепта такой теории есть все необходимое:
демон Chromium в операционной системе, который подключает устройство автоматически;
исходный код драйвера, который всегда располагается на локальной машине;
background-страница, которая выполняет все необходимые операции с устройством, в зависимости от заданных условий.
Но не буду отвлекаться, продолжу и вернусь к свойствам кастомного драйвера.
Драйвер обладает вводом-выводом со специфичным устройством. А также должен понимать протокол конкретного устройства и его возможности.
Согласно тексту Спецификации, API WebUSB обладает почти всеми необходимыми методами для этого требования. Они почти аналогичны методам, используемым в библиотеке libusb.c, являющейся почти дефолтным решением для всех операционных систем.
Для примера: объект USBDevice
из интерфейса WebUSB включает уже необходимый минимум для работоспособности драйвера:
для стандартных типов запросов (контрольных) controlTransferIn() и controlTransferOut();
для обмена данными с endpoint-ами устройства transferIn() и transferOut().
Разработчику остается только определиться с описанием протокола устройства. Но чаще всего, если речь идет о кастомном драйвере, имея доступ к вышеуказанным методам, решить задачу не составит труда, поскольку такие протоколы все задокументированы согласно классу устройств, описанному в конфигурационном дескрипторе прошивки USB-устройства.
Тема с конфигурационным дескриптором в контексте API WebUSB достаточно объемная, поскольку затрагивает описания стандартов от USB-IF и выносится за рамки нашей небольшой теории, поэтому здесь такие вопросы мы не рассматриваем.
Описанный спецификацией стандарт API WebUSB отвечает всем определенным выше требованиям для кастомного драйвера с одним лишь «ма-а-а-аленьким» условием — если разработчик возьмет за среду исполнения браузерное окружение вместо операционной системы, на котором будет выполняться скрипт на JavaScript. То есть, будет учитывать все особенности браузера и условия его эксплуатации.
Это единственное условное исключение является итоговым в настоящих теоретических изысканиях и является всем фундаментальным условием при построении архитектуры потенциального абстрактного приложения с применением кастомного драйвера на основе API WebUSB.
Если и в таком виде пояснения остаются непонятными, то вот вам картинка:

Как видно из изображения, подход к реализации через API WebUSB не отличается от классического подхода, нужно только брать во внимание отличие окружения исполнения. Благодаря подобному совпадению написание драйвера USB-устройства под силу даже веб-разработчику.
Заключение
Теория — это залог концепта, с построением которого вам когда-нибудь предстоит столкнуться при исследовании очередного решения бизнес-задачи.
Не всегда нужно сломя голову бежать писать код, иногда вначале нужно ответить на вопрос: «А вообще можно ли такое сделать?». И только рассмотрев этот вопрос, можно лучше понять границы дозволенного в архитектуре вашего приложения будущего.