Статья будет полезна архитекторам и опытным front-end разработчикам систем масштаба предприятий, столкнувшихся с проблемой доступа к периферийному оборудованию из тонкого клиента своей системы.
И да, скажем сразу, задача усложняется тем, что стандартные подходы с применением ActiveX, Java Applet, плагина браузера нас не устраивают по соображениям безопасности, универсальности и сложностей с управляемостью и сопровождаемостью.
Представьте себе скромный (по китайским меркам) банк, в отделениях которого работают более 100 тыс. операторов. У них есть рабочие места, на которых они обслуживают клиентов, а к рабочим местам подключены различные периферийные устройства:
- Сетевой/локальный принтер.
- Чековый принтер (Epson-M950 или Olivetti).
- POS-терминал (Verifone VX820).
- Устройство для чтения «таблеток» (touch memory с ЭЦП).
- Сканеры разных типов (для штрих-кодов, паспортов или просто документов).
- Веб-камера (фотографировать потенциальных заемщиков).
- Специфическое банковское оборудование – cash dispenser/receiver и т.п.
Внушительный список, не правда ли? И со всем этим нужно взаимодействовать из работающего в браузере react-приложения.
Работа с устройствами на низком уровне осуществляется через драйвера производителей оборудования или native библиотеки внутренней разработки. Установка и обновление драйверов и другого ПО на рабочих местах осуществляются централизованно. На рабочих местах стоят Windows и Internet Explorer 11 или 8. Обсуждается возможность перехода на Linux и Chrome/Firefox. Отсюда возникает требование кросс-платформенности и кросс-браузерности.
Проблема разнородности устройств и их отказов более-менее решена, но требуется мониторинг работы устройств, включая возможность забирать логи с рабочего места. Также требуется централизованное управление настройками работы с периферией.
Требования безопасности заключаются в контроле целостности кода, запускаемого на клиенте, ограничении доступа к периферии (доступ должен быть только с локального рабочего места) и ряда специфичных требований к работе с touch memory.
Отдельный вопрос – работа планшета с периферией. Идея в том, чтобы заменить компьютеры на мобильные девайсы, перенеся на них весь функционал оператора, включая совершение банковских операций. В этой статье мы не будем подробно говорить про планшеты, обязательно расскажем об этом в другой статье, просто обозначим, что здесь возникают как вопросы подключения самого планшета к внутренней банковской сети по wi-fi, так и вопросы работы с устройствами, подключаемыми непосредственно к планшету, например, mPOS-терминалами.
Как мы пытались все это приручить, почему не сразу получилось
Из условий эксплуатации следует схема работы с периферией:
Мы попытались найти решение проблемы и проработали несколько вариантов.
HTML5
Хотя в нем потенциально есть работа с периферийными устройствами, текущие реализации заточены под мобильные устройства или аудио/видео, и для наших задач не подходят от слова «совсем».
WebUSB
https://wicg.github.io/webusb/ — во-первых, не все устройства подключаются через USB. Во-вторых, поддержки WebUSB на текущий момент нет нигде, кроме экспериментальной feature в Chrome. Так что тоже нам не подходит.
ActiveX
Способ старинный и проверенный – делаются обертки над драйверами, устанавливаемые как локальные OCX или DLL, обращение к которым идет через ActiveXObject:
var printer = new ActiveXObject("LocalPrinterComponent.Printer");
printer.print(data + "\r\n");
Но он работает только в IE и Windows. Несмотря на попытки сделать технологию ActiveX переносимой, Microsoft отказалась от развития ActiveX в пользу технологии плагинов.
Плагины браузера
Это тоже нам не подходит, не является целевым, привязывает к браузеру и ограничивает универсальность.
Нативные компоненты на Java
На localhost выставляется cервис, который доступен из JavaScript. От этого тоже решили отказаться, т.к. реализация более трудоемкая, требуется использовать веб-сервер либо писать свой.
Applet
Не целевой, возникают проблемы с правами доступа на локальном устройстве.
И что же делать?
В итоге мы остановились на следующей архитектуре работы с периферийными устройствами:
Клиентское приложение в JavaScript обращается к локальному сервису через модуль работы с периферийным API, который является обвязкой над клиентской частью socket.io.
На рабочие места устанавливается node.js, который запускается под сервисной учетной записью при старте операционки. В node.js работает наш модуль bootstrap, отвечающий за загрузку npm-модулей для работы с периферийными устройствами с сервера в локальную файловую систему. Клиентский код генерирует event, в качестве атрибутов передается код и версия модуля, который работает с устройством, вызываемый метод и его параметры:
<html>
<head>
<title>Test hello.socket.io</title>
<script src="socket.io.min.js"></script>
<script>
var socket = io.connect('http://localhost:8055');
socket.on('eSACExec', function (data) {
var newLi = document.createElement('li');
newLi.style.color = "green";
newLi.innerHTML = data.data + ' from ' + data.from;
document.getElementById("list").appendChild(newLi);
console.log(data.data);
});
socket.on('eSACOnError', function (data) {
var newLi = document.createElement('li');
newLi.style.color = "red";
newLi.innerHTML = data.message;
document.getElementById("list").appendChild(newLi);
console.log(data.error);
});
function sendHello() {
socket.emit('eSASExec', {module: 'api.system.win.window', version:'0.0.1',
repoURL: 'git+https://github.com/Gromatchikov/',
method: 'sayHello', params: {hello: 'Server API'}});
}
</script>
</head>
<body>
<ol id="list">
</ol>
<button onclick="sendHello()">send</button>
</body>
</html>
Также bootstrap отвечает за работу с платформенными сервисами (выгрузку логов с рабочего места по запросу администратора и т.п.)
Для каждого периферийного устройства имеется свой модуль работы с ним, исполняемый в node.js. Bootstrap проксирует вызов в метод модуля:
var http = require('http');
var sio = require('socket.io');
var bootstrapSA = require('./bootstrap.js');
var modules = new Object();
function SAServer() {
if (!(this instanceof SAServer)) {
return new SAServer();
}
this.app = http.createServer();
this.sioApp = sio.listen(this.app);
this.sioApp.sockets.on('connection', function (client) {
client.on('eSASExec', function (data) {
try {
console.info('SAserver event eSASExec:');
console.info(data);
bootstrapSA.demandModuleProperty(modules, data.repoURL, data.module, data.version, function (module) {
var fullModuleName = bootstrapSA.getFullName(data.module, data.version)
var api = modules[fullModuleName];
if (api != undefined) {
var result = api[data.method](data.params);
console.info('Result eSASExec', result);
client.emit('eSACExec', {data: result || '', from: fullModuleName});
} else {
console.error('[', 'Error ', fullModuleName, '] is ', undefined);
client.emit('eSACOnError', { message: 'Error define api module' + fullModuleName});
}
});
} catch (_error) {
console.error('[', 'Error eSASExec', ( _error.code ? _error.code : ''), ']', _error);
client.emit('eSACOnError', { message: 'Error :(', error: _error.stack || ''});
}
});
client.on('disconnect', () => {
console.log('User disconnected');
});
client.on('eSASStop', () => {
process.exit(0);
});
console.info('User connected');
});
console.info('System API server loaded');
}
//запуск сервера
SAServer.prototype.start = function startSAServer(port) {
// диалоговое окно
this.currentPort = port;
if (this.currentPort == undefined) {
this.currentPort = process.env.npm_package_config_port;
}
this.app.listen(this.currentPort);
console.log('System API server running at http://127.0.0.1:'+ this.currentPort);
bootstrapSA.inspect('parent object', modules);
};
module.exports = SAServer;
Модуль работает с низкоуровневым API операционной системы или драйвера через npm модуль node.js «ffi»:
// функция преобразования строки JavaScript (UTF-8) в UTF-16
function TEXT(text){
return new Buffer(text, 'ucs2').toString('binary');
}
var FFI = require('ffi');
// подключаемся к user32.dll
var user32 = new FFI.Library('user32', {
'MessageBoxW': [
'int32', [ 'int32', 'string', 'string', 'int32' ]
]
});
function WindowSA() {
if (!(this instanceof WindowSA)) {
return new WindowSA();
}
console.log('Window system API module loaded');
}
WindowSA.prototype.sayHello = function sayHello(params) {
// диалоговое окно
var OK_or_Cancel = user32.MessageBoxW(0, TEXT('Привет, "' + params.hello + '"!'), TEXT('Заголовок окна'), 1);
};
module.exports = WindowSA;
Когда клиентское приложение обращается к bootstrap, передавая модуль и версию, bootstrap проверяет локальное хранилище. Если нужного модуля нет в локальном хранилище, он выкачивается его с сервера. Таким образом, централизованно устанавливаются только драйвера и node.js с bootstrap’ом, а npm-модули для работы с устройствами скачиваются в рантайме. Но данная функция вряд ли будет использоваться в промышленной конфигурации, так что предполагаем, что при удаленной инсталляции драйверов устройств на рабочие места сотрудников будет устанавливаться и соответствующий npm модуль периферийного API, представляющий собой JS bundle.
var loadModule = function (obj, repoURL, name, version, callback) {
var mod;
try {
//todo mod = require(fullModuleName); и ограничить версию major.minor для накатки fix
console.log('System API module "%s" require...', getFullName(name, version));
mod = new require(name)();
return mod;
} catch (err){
errFlag = true;
console.error('Error require', err.code, err);
if (err.code == 'MODULE_NOT_FOUND') {
installModule(obj, repoURL, name, version, callback);
}
}
}
//Установить свойство и модуль, если ранее не был установлен
//todo установка зависимости на модуль с версией name@version (fullModuleName)
var demandModuleProperty = function (obj, repoURL, name, version, callback) {
var fullModuleName = getFullName(name, version);
var errFlag = false;
if (!obj.hasOwnProperty(fullModuleName)) {
console.log('System API module "%s" defining', fullModuleName);
var mod = loadModule(obj, repoURL, name, version, callback);
if (mod == undefined){
return;
}
Object.defineProperty(obj, fullModuleName, {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(obj, fullModuleName, {
configurable: false,
enumerable: true,
value: mod
});
console.log('System API module "%s" defined', fullModuleName);
inspect('parent object', obj);
inspect(fullModuleName, obj[fullModuleName]);
}
});
}
if (!errFlag) {
console.log('Callback for "%s"', fullModuleName);
callback(obj[fullModuleName]);
}
};
//установка модуля из репозитория
//todo установка модуля в папку с версией name@version, ограничить версию major.minor для накатки fix
var installModule = function(obj, repoURL, name, version, callback){
console.log('Installing module %s version %s', name, version);
var fullURL = getFullURL(repoURL, name, version);
npm.load({progress: true, '--save-optional': true, '--force': true, '--ignore-scripts': true},function(err) {
// handle errors
// install module
npm.commands.install([fullURL], function(er, data) {
// log errors or data
if (!er){
console.info('System API module "%s" installed', name);
//повторная попытка определения свойства
demandModuleProperty(obj, repoURL, name, version, callback);
} else {
console.error('Error NPM Install', er.code, er);
}
});
npm.on('log', function(message) {
// log installation progress
console.log('NPM logs:' + message);
});
});
};
Решение пока еще не реализовано, мы работаем над этим и обязательно расскажем о результатах в другой статье. А пока хотим спросить у вас, что вы думаете о выбранном подходе? Какие подводные камни нас ждут? Приглашаем всех принять участие в дискуссии в комментариях.
Комментарии (39)
zimyx
15.06.2017 18:32+3На localhost выставляется cервис, который доступен из JavaScript. От этого тоже решили отказаться, т.к. реализация более трудоемкая, требуется использовать веб-сервер либо писать свой.
И в итоге использовали веб-сервер.
Java отлично бежит из одной Jar с использованием Spring Boot или Vert.x.
Почему тогда не .Net?LiguidCool
15.06.2017 22:29Веб сокеты, а это двухсторонний обмен, т.е. не совсем веб :)
zimyx
15.06.2017 23:55Обе названные мной технологии для Java умеют включать websocket как в голом виде, так и с прослойками вроде STOMP или sockJS при сравнимом объёме конфигурационного кода.
fc_arny
15.06.2017 19:47+3К сожалению кроме как поднять локально сервер для работы со сканерами дактилоскопии не смогли ничего придумать… Хотели тонкий клиент вместо толстого, а получился утолщенный.
Lanjusto
15.06.2017 20:37А можете чуть подробнее рассказать, как предполагается работать с устройствами, которыми сами инициируют события (а не отвечают на запрос JS-приложения в браузере)? Тот же сканер например.
И что будет, если пользователь откроет JS-приложение в нескольких вкладках браузера, а потом отсканирует штриход?
LiguidCool
15.06.2017 22:25+1Там же веб сокеты, это не веб сервер. Так что ни с тем, ни с тем проблем не будет.
Balek
20.06.2017 14:45И что будет, если пользователь откроет JS-приложение в нескольких вкладках браузера, а потом отсканирует штриход?
Я делаю так: страница загружает SharedWorker, который уже устанавливает WebSocket-соединение с драйвером. По событиям focus/blur на window, страница отправляет сообщения в воркер. Таким образом воркер знает, какой из страниц отправлять событие от драйвера. Плюс есть возможность реагировать на событие специальным образом, если ни одна из страниц не активна в данный момент.
jehy
15.06.2017 21:39Довольно очевидное решение, которое будет работать — почему бы и нет. Сам периодически такое пишу. Например, программа для прошивки MPOS терминалов написана именно на node.js — настройка в браузере, потом вызов локальной ноды, которая уже дёргает вендорское приложение или нативные мультиплатформенные модули. В плюсах — есть интерфейс, удобно работать, легко поддерживать, не надо размываться на ещё один стек. Минусов пока не обнаружено.
По вашему коду — непонятно, почему у вас в 2017 году древний javascript и callback hell как он есть.
GAleksey
16.06.2017 22:54рабочий прототип. JS при написании модулей можно заменить TypeScript для удобства разработчика, и уже из него генерировать JS
ertaquo
15.06.2017 22:09+1Что-то вы намудрили. В начале статьи казалось, что вы хотите обойтись чисто браузером, безо всяких устанавливаемых приложений, но в итоге пришли к запуску node.js на каждом компьютере, да еще и под серверным аккаунтом.
На вашем месте я бы написал приложение со своим HTTP API, на любом языке, для которого есть библиотеки для работы с нужным оборудованием. Да, примерно как сейчас, но без серверного яваскрипта, все-таки не думаю, что он подходит для работы с кучей разного оборудования :)
BigDflz
15.06.2017 23:06не рассматривали JWS (java web start)? написать свой сервер — вот пример blindscanner.com/ru, вполне прикольная штучка. устанавливается просто.
BigDflz
16.06.2017 06:27прошу прщения — не правильная ссылка была выложена, Вот правильная unit6.ru/twain-web. (это не раклама, а пример удачного решения, на мой взгляд) Решаемая задача — аналогичная, использование web для подключения к железу. Причём устанавливаемый «сервер» доступен не только локальному компу, но и по сети.
LiguidCool
15.06.2017 23:06Как взаимодействуют Браузер и сервер ясно, но было бы интересно про взаимодействие сервера и самой периферии. Какие разъемы, протоколы итп.
GAleksey
16.06.2017 22:51npm модуль ffi для вызова системной библиотеки — драйвера устройства или API ОС
CarambaPirat
15.06.2017 23:06Стоило ли использовать socket.io в проекте? Мне кажется и без него вы могли реализовать WebSockets.
Конечно оно проще заюзать эту либу, но смысл? Ни в браузере, ни на сервере оно не нужно. Реализовать можно и без этой «библиотеки для чата».
И вообще: https://github.com/uNetworking/uWebSockets
Пишите на C++/С. Не нужна будет node с серверным аккаунтом.
dskopylov
15.06.2017 23:06Вставлю свои пять копеек. Наверное самый главный аргумент против — это использование JS для работы со всем этим банковским зоопарком оборудования. Та же Java (на которой СберТех сейчас активно пишет, насколько я понимаю), или .NET, всяко лучше будут интегрироваться с этим железом. Получается что JS-ников у вас больше чем Java-истов:-) А так еще вот что хочется спросить — канал связи браузера с локальным WebSocket-сервером будет как-то защищаться (SSL)? Я голосую за Java и выкачивание актуальных модулей подключения к банковскому железу (JAR-ников) с внутреннего репозитория при каждой загрузке локальной среды исполнения перефирийного ПО (= включению компа).
BigDflz
16.06.2017 06:14А так еще вот что хочется спросить — канал связи браузера с локальным WebSocket-сервером будет как-то защищаться (SSL)?
для этих целей существует wss/
Я голосую за Java и выкачивание актуальных модулей подключения к банковскому железу (JAR-ников) с внутреннего репозитория при каждой загрузке локальной среды исполнения перефирийного ПО (= включению компа).
технология называется JWS.
kirillaristov
15.06.2017 23:18+1Банк, в отделениях которого работают более 100 тыс. операторов. У них есть рабочие места, на которых они обслуживают клиентов.
Вы меня простите пожалуйста, но совсем недавно один человек из некоего российского банка на моих глазах недоумевал с людей, толкающихся в оффлайновых банках, в то время как можно все операции делать удаленно.
Это же электронные деньги. Зачем мне везти свое тело в банк, если я могу все сделать через приложение на смартфоне не выходя из зоны комфорта?BigDflz
16.06.2017 06:19есть куча вопросов, которые можно решить только в офисе банка.
kirillaristov
16.06.2017 07:00Роль офиса играет приложение на смартфоне/личный кабинет на сайте банка.
Ребята из одного банка действительно постарались, чтобы всё было удобно.
Пример: хочу открыть расчётный счёт для ИП. Захожу в красный банк: нужно предоставить аж 10 документов (я серьёзно, у них до сих пор висит пдфка с 11 пунктами). А в жёлтом просят только ИНН и паспортные данные, ОГРН курьер спрашивает когда привозит уже готовую карту. В итоге пользуюсь 5+ лет, потребность съездить в офис ни разу не возникала.
zim32
15.06.2017 23:50Тоже не пойму почему вместо HTTP врапера на с++ или GO к примеру вы выбрали ноду которую надо ставить на «более 100 тыс. операторов. „
Xao
16.06.2017 08:53WebUSB это только в хроме фича, или в стандарт / другие браузеры проберётся?
Оно доступно везде, или только для расширений?
Находила также, что есть что-то для работы с HID, но, вроде бы тоже только для хрома/расширений и только в экспериментальной версии.
sidristij
16.06.2017 11:22Я делал через JS <-> socket.io <-> Windows Service (C#) Тоже хорошо. Надо было соединять с Bluetooth, USB, Ethernet через спец драйвера
Hacksli
16.06.2017 11:52Мы лично для себя не смогли придумать что-то более адекватного чем orange pi с сервером на борту для общения с переферией
irbis_al
16.06.2017 11:52У меня давно периферия(сканеры штрихкода, весы, фискальные регистраторы) может через браузер работать используя для коммуникации websocket.
Который можно открыть как ws://localhost
И кроспплатформенный драйвер (на java или node ) читает локальные(а может и другого компа) rs-232 и «кричит в websocket»… и браузер обрабатывает.
LowHP
16.06.2017 19:27+1Был опыт разработки чего-то подобного. Возможно не «подводный камень» но все же вставлю свои 5 копеек, которые немного нам подпортили крови в свое время:
В данном решении необходимо помнить, что после того, как вы начнете использовать https для веб-страницы (зачастую сайт переводят на https в последний момент перед релизом), вы не сможете делать вызовы на ваш node.js сервер, работающий по http из-за Mixed Content.
В свою очередь, т.к. вы совершаете https вызов из браузера на localhost, вам необходимо чтобы этот localhost имел валидный SSL сертификат issued to «localhost», выданный trusted Certificate authority. Понятное дело, что никакой Certificate authority такой сертификат не выдаст.
И тут начинаются костыли и выбор из меньших зол:
1. Можно сгенерировать self-signed certificate для Certificate authority. Потом подписать этим сертификатом SSL сертификат на localhost. Теперь у вас есть SSL сертификат на localhost, но при этом вам придется устанавливать self-signed certificate c certificate authority в trusted root сертификаты вашей системы. Уверен корпоративные безопасники это по достоинству оценят. Вот тут есть информация про скандал с Dell, который установил свои сертификаты в trusted root и чем это закончилось.
2. Можно купить валидный сертификат на некоторый домен например example.com. Использовать его в node.js сервере, при этом в hosts файле переписать example.com на localhost. Что тоже ужасно.
Будет интересно от Вас услышать, как решите эту проблему у себя. Конечно при условии что вы будете использовать https для своего сайта.BigDflz
17.06.2017 08:32проблема существует, но если использовать websocket для связи с localhost, то всё сводится к установке самоподписанного сертификата для wss на локальный комп. Это несколько не камильфо, но для корпоративного использования можно. на всё остальное это не повлияет.
Balek
20.06.2017 14:55Решаю проблему модифицированным вторым способом. Есть сервер, который для localhost.example.com получает сертификат от LetsEncrypt и выкладывает его с ключом на HTTP-сервере. При запуске драйвер запрашивает новый сертификат и сохраняет его. В случае проблем с сервером, сохраненной копией можно будет пользоваться ещё 3 месяца. Остается также в hosts прописать localhost.example.com.
zam0th
19.06.2017 08:43Предпосылка о переносимости изначально неверна, просто потому что драйвера непереносимы по определению. Соответственно, можно перестать заниматься придумыванием способа исполнения нативного кода
через жепуиз js и
1) под windows написать сервис на любом языке (напр. javase с оберткой), который локально под управлением svchost будет выполнять те же самые функции, но без извращений. Как бонус получаем централизованное обновление сервиса через ad (про что вы конечно же забыли). Под linux это на ура переносится на systemd, после того, естественно, когда все драйвера перепишутся. Про jws уже выше предлагали, это в принципе то же самое, только без управления.
или
2) запиливаются плагины под браузер (целевой в банке — ie11, а слова про линукс и хром — это, пардон, розовые мечты апологетов js) и все работает. Обновление плагинов осуществляется опять же централизованно через ad. У CryptoPro плагины, например, уже есть. Фраза «Это тоже нам не подходит, не является целевым, привязывает к браузеру и ограничивает универсальность.» является сомнительной демагогией.GAleksey
19.06.2017 13:191. Раскатка сейчас через microsoft sccm предполагается и подготовка пакета будет включать в себя драйвер и обвязку на JS (npm пакет модуля работы с периферией). Раскатка и поддержка самой JVM, не отличается от Node.JS. С Linux будет похожая история. Пока явных плюсов в написании модулей на Java не вижу.
2. От плагинов отказались намерено, о чем написали в начале статьи. Также были предложения писать свой браузер на основе готовых платформ…
denismaster
Статья интересная, но оформление кода картинками — не очень хорошая затея. Лучше текстом)
Rastishka
Красиво подсвеченный код картинкой как раз неплохо (если цели копировать нет), но вот jpeg и криво вырезанные скриншоты это ужасно, создают представление о «профессионализме» компании… =/
Koobeton
Чему вы удивляетесь, это же ЕФС:
Epic
Fail
System
Код в jpeg картинках — это феерический звездец!
EFS_programm
А вы повторяетесь)
Судя по профилю, вы горячий поклонник блога Программы ЕФС и активный пользователь Хабра.
Пожалуйста, расскажите о своем опыте по тематике публикации, какие темы для вас наиболее интересны?
GAleksey
примеры кода можно посмотреть тут: периферийное API и работа с окнами в windows
EFS_programm
Спасибо за комментарий, доработали