Работа сотрудника банка опасна и трудна. В Единой фронтальной системе мы стараемся помочь сотруднику банка и автоматизировать его работу. Одна из многочисленных задач, которую нам нужно решить, — доработать тонкий клиент для возможности работы с периферийным банковским оборудованием. Но сделать это не так-то просто. В данной статье мы расскажем, как мы пытались решить задачу, и к чему это привело.
Статья будет полезна архитекторам и опытным front-end разработчикам систем масштаба предприятий, столкнувшихся с проблемой доступа к периферийному оборудованию из тонкого клиента своей системы.

И да, скажем сразу, задача усложняется тем, что стандартные подходы с применением ActiveX, Java Applet, плагина браузера нас не устраивают по соображениям безопасности, универсальности и сложностей с управляемостью и сопровождаемостью.
Представьте себе скромный (по китайским меркам) банк, в отделениях которого работают более 100 тыс. операторов. У них есть рабочие места, на которых они обслуживают клиентов, а к рабочим местам подключены различные периферийные устройства:
Внушительный список, не правда ли? И со всем этим нужно взаимодействовать из работающего в браузере react-приложения.
Работа с устройствами на низком уровне осуществляется через драйвера производителей оборудования или native библиотеки внутренней разработки. Установка и обновление драйверов и другого ПО на рабочих местах осуществляются централизованно. На рабочих местах стоят Windows и Internet Explorer 11 или 8. Обсуждается возможность перехода на Linux и Chrome/Firefox. Отсюда возникает требование кросс-платформенности и кросс-браузерности.
Проблема разнородности устройств и их отказов более-менее решена, но требуется мониторинг работы устройств, включая возможность забирать логи с рабочего места. Также требуется централизованное управление настройками работы с периферией.
Требования безопасности заключаются в контроле целостности кода, запускаемого на клиенте, ограничении доступа к периферии (доступ должен быть только с локального рабочего места) и ряда специфичных требований к работе с touch memory.
Отдельный вопрос – работа планшета с периферией. Идея в том, чтобы заменить компьютеры на мобильные девайсы, перенеся на них весь функционал оператора, включая совершение банковских операций. В этой статье мы не будем подробно говорить про планшеты, обязательно расскажем об этом в другой статье, просто обозначим, что здесь возникают как вопросы подключения самого планшета к внутренней банковской сети по wi-fi, так и вопросы работы с устройствами, подключаемыми непосредственно к планшету, например, mPOS-терминалами.
Из условий эксплуатации следует схема работы с периферией:
Мы попытались найти решение проблемы и проработали несколько вариантов.
Хотя в нем потенциально есть работа с периферийными устройствами, текущие реализации заточены под мобильные устройства или аудио/видео, и для наших задач не подходят от слова «совсем».
https://wicg.github.io/webusb/ — во-первых, не все устройства подключаются через USB. Во-вторых, поддержки WebUSB на текущий момент нет нигде, кроме экспериментальной feature в Chrome. Так что тоже нам не подходит.
Способ старинный и проверенный – делаются обертки над драйверами, устанавливаемые как локальные OCX или DLL, обращение к которым идет через ActiveXObject:
Но он работает только в IE и Windows. Несмотря на попытки сделать технологию ActiveX переносимой, Microsoft отказалась от развития ActiveX в пользу технологии плагинов.
Это тоже нам не подходит, не является целевым, привязывает к браузеру и ограничивает универсальность.
На localhost выставляется cервис, который доступен из JavaScript. От этого тоже решили отказаться, т.к. реализация более трудоемкая, требуется использовать веб-сервер либо писать свой.
Не целевой, возникают проблемы с правами доступа на локальном устройстве.
В итоге мы остановились на следующей архитектуре работы с периферийными устройствами:
Клиентское приложение в JavaScript обращается к локальному сервису через модуль работы с периферийным API, который является обвязкой над клиентской частью socket.io.
На рабочие места устанавливается node.js, который запускается под сервисной учетной записью при старте операционки. В node.js работает наш модуль bootstrap, отвечающий за загрузку npm-модулей для работы с периферийными устройствами с сервера в локальную файловую систему. Клиентский код генерирует event, в качестве атрибутов передается код и версия модуля, который работает с устройством, вызываемый метод и его параметры:
Также bootstrap отвечает за работу с платформенными сервисами (выгрузку логов с рабочего места по запросу администратора и т.п.)
Для каждого периферийного устройства имеется свой модуль работы с ним, исполняемый в node.js. Bootstrap проксирует вызов в метод модуля:
Модуль работает с низкоуровневым API операционной системы или драйвера через npm модуль node.js «ffi»:
Когда клиентское приложение обращается к bootstrap, передавая модуль и версию, bootstrap проверяет локальное хранилище. Если нужного модуля нет в локальном хранилище, он выкачивается его с сервера. Таким образом, централизованно устанавливаются только драйвера и node.js с bootstrap’ом, а npm-модули для работы с устройствами скачиваются в рантайме. Но данная функция вряд ли будет использоваться в промышленной конфигурации, так что предполагаем, что при удаленной инсталляции драйверов устройств на рабочие места сотрудников будет устанавливаться и соответствующий npm модуль периферийного API, представляющий собой JS bundle.
Решение пока еще не реализовано, мы работаем над этим и обязательно расскажем о результатах в другой статье. А пока хотим спросить у вас, что вы думаете о выбранном подходе? Какие подводные камни нас ждут? Приглашаем всех принять участие в дискуссии в комментариях.
Статья будет полезна архитекторам и опытным 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); }); }); };
Решение пока еще не реализовано, мы работаем над этим и обязательно расскажем о результатах в другой статье. А пока хотим спросить у вас, что вы думаете о выбранном подходе? Какие подводные камни нас ждут? Приглашаем всех принять участие в дискуссии в комментариях.

