Работа сотрудника банка опасна и трудна. В Единой фронтальной системе мы стараемся помочь сотруднику банка и автоматизировать его работу. Одна из многочисленных задач, которую нам нужно решить, — доработать тонкий клиент для возможности работы с периферийным банковским оборудованием. Но сделать это не так-то просто. В данной статье мы расскажем, как мы пытались решить задачу, и к чему это привело.
Статья будет полезна архитекторам и опытным 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);
});
});
};
Решение пока еще не реализовано, мы работаем над этим и обязательно расскажем о результатах в другой статье. А пока хотим спросить у вас, что вы думаете о выбранном подходе? Какие подводные камни нас ждут? Приглашаем всех принять участие в дискуссии в комментариях.