From de9e76a70e0702cba2c45256938f36cfbce77183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Mon, 18 Jun 2018 15:35:18 +0200 Subject: [PATCH] Improve socket implementation --- src/commands/getIsGenuine.js | 2 +- src/helpers/apps/installApp.js | 2 +- src/helpers/apps/uninstallApp.js | 2 +- src/helpers/common.js | 160 ++-------------------------- src/helpers/devices/getIsGenuine.js | 2 +- src/helpers/socket.js | 128 ++++++++++++++++++++++ src/logger.js | 8 ++ static/i18n/en/errors.yml | 5 + 8 files changed, 152 insertions(+), 157 deletions(-) create mode 100644 src/helpers/socket.js diff --git a/src/commands/getIsGenuine.js b/src/commands/getIsGenuine.js index 8b9cfa2e..c0b66b8b 100644 --- a/src/commands/getIsGenuine.js +++ b/src/commands/getIsGenuine.js @@ -6,7 +6,7 @@ import { fromPromise } from 'rxjs/observable/fromPromise' import getIsGenuine from 'helpers/devices/getIsGenuine' import { withDevice } from 'helpers/deviceAccess' -type Input = * +type Input = * // FIXME ! type Result = string const cmd: Command = createCommand('getIsGenuine', ({ devicePath, targetId }) => diff --git a/src/helpers/apps/installApp.js b/src/helpers/apps/installApp.js index 2cdd04a3..1998cfb5 100644 --- a/src/helpers/apps/installApp.js +++ b/src/helpers/apps/installApp.js @@ -11,6 +11,6 @@ import type { LedgerScriptParams } from 'helpers/common' export default async function installApp( transport: Transport<*>, { appParams }: { appParams: LedgerScriptParams }, -): Promise { +): Promise<*> { return createSocketDialog(transport, '/install', appParams) } diff --git a/src/helpers/apps/uninstallApp.js b/src/helpers/apps/uninstallApp.js index c9f4fc44..c67c029c 100644 --- a/src/helpers/apps/uninstallApp.js +++ b/src/helpers/apps/uninstallApp.js @@ -11,7 +11,7 @@ import type { LedgerScriptParams } from 'helpers/common' export default async function uninstallApp( transport: Transport<*>, { appParams }: { appParams: LedgerScriptParams }, -): Promise { +): Promise<*> { const params = { ...appParams, firmware: appParams.delete, diff --git a/src/helpers/common.js b/src/helpers/common.js index 7d96e48b..a5a28c8c 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -1,24 +1,13 @@ // @flow -import chalk from 'chalk' -import Websocket from 'ws' +// FIXME remove this file! 'helpers/common.js' RLY? :P + import qs from 'qs' import type Transport from '@ledgerhq/hw-transport' +import { createDeviceSocket } from './socket' import { BASE_SOCKET_URL, APDUS, MANAGER_API_URL } from './constants' -type WebsocketType = { - send: (string, any) => void, - on: (string, Function) => void, -} - -type Message = { - nonce: number, - query?: string, - response?: string, - data: any, -} - export type LedgerScriptParams = { firmware?: string, firmwareKey?: string, @@ -35,59 +24,6 @@ export async function getMemInfos(transport: Transport<*>): Promise { return createSocketDialog(transport, '/get-mem-infos', { targetId, perso: 'perso_11' }) } -/** - * Send data through ws - */ -function socketSend(ws: WebsocketType, msg: Message) { - logWS('SEND', msg) - const strMsg = JSON.stringify(msg) - ws.send(strMsg) -} - -/** - * Exchange data on transport - */ -export async function exchange( - ws: WebsocketType, - transport: Transport<*>, - msg: Message, -): Promise { - const { data, nonce } = msg - const r: Buffer = await transport.exchange(Buffer.from(data, 'hex')) - const status = r.slice(r.length - 2) - const buffer = r.slice(0, r.length - 2) - const strStatus = status.toString('hex') - socketSend(ws, { - nonce, - response: strStatus === '9000' ? 'success' : 'error', - data: buffer.toString('hex'), - }) -} - -/** - * Bulk update on transport - */ -export async function bulk(ws: WebsocketType, transport: Transport<*>, msg: Message) { - const { data, nonce } = msg - - // Execute all apdus and collect last status - let lastStatus = null - for (const apdu of data) { - const r: Buffer = await transport.exchange(Buffer.from(apdu, 'hex')) - lastStatus = r.slice(r.length - 2) - } - if (!lastStatus) { - throw new Error('No status collected from bulk') - } - - const strStatus = lastStatus.toString('hex') - socketSend(ws, { - nonce, - response: strStatus === '9000' ? 'success' : 'error', - data: strStatus === '9000' ? '' : strStatus, - }) -} - /** * Open socket connection with firmware api, and init a dialog * with the device @@ -97,56 +33,10 @@ export async function createSocketDialog( endpoint: string, params: LedgerScriptParams, managerUrl: boolean = false, -) { - return new Promise(async (resolve, reject) => { - try { - let lastData - const url = `${managerUrl ? MANAGER_API_URL : BASE_SOCKET_URL}${endpoint}?${qs.stringify( - params, - )}` - - log('WS CONNECTING', url) - const ws: WebsocketType = new Websocket(url) - - ws.on('open', () => log('WS CONNECTED')) - - ws.on('close', () => { - log('WS CLOSED') - resolve(lastData) - }) - - ws.on('message', async rawMsg => { - const handlers = { - exchange: msg => exchange(ws, transport, msg), - bulk: msg => bulk(ws, transport, msg), - success: msg => { - if (msg.data) { - lastData = msg.data - } else if (msg.result) { - lastData = msg.result - } - }, - error: msg => { - log('WS ERROR', ':(') - throw new Error(msg.data) - }, - } - try { - const msg = JSON.parse(rawMsg) - if (!(msg.query in handlers)) { - throw new Error(`Cannot handle msg of type ${msg.query}`) - } - logWS('RECEIVE', msg) - await handlers[msg.query](msg) - } catch (err) { - log('ERROR', err.toString()) - reject(err) - } - }) - } catch (err) { - reject(err) - } - }) +): Promise { + console.warn('DEPRECATED createSocketDialog: use createDeviceSocket') // eslint-disable-line + const url = `${managerUrl ? MANAGER_API_URL : BASE_SOCKET_URL}${endpoint}?${qs.stringify(params)}` + return createDeviceSocket(transport, url).toPromise() } /** @@ -169,42 +59,6 @@ export async function getFirmwareInfo(transport: Transport<*>) { } } -/** - * Debug helper - */ -export function log(namespace: string, str: string = '', color?: string) { - namespace = namespace.padEnd(15) - // $FlowFixMe - const coloredNamespace = color ? chalk[color](namespace) : namespace - if (__DEV__) { - console.log(`${chalk.bold(`> ${coloredNamespace}`)} ${str}`) // eslint-disable-line no-console - } -} - -/** - * Log a socket send/receive - */ -export function logWS(type: string, msg: Message) { - const arrow = type === 'SEND' ? '↑' : '↓' - const namespace = `${arrow} WS ${type}` - const color = type === 'SEND' ? 'blue' : 'red' - if (msg.nonce) { - let d = '' - if (msg.query === 'exchange') { - d = msg.data.length > 100 ? `${msg.data.substr(0, 97)}...` : msg.data - } else if (msg.query === 'bulk') { - d = `[bulk x ${msg.data.length}]` - } - log( - namespace, - `${String(msg.nonce).padEnd(2)} ${(msg.response || msg.query || '').padEnd(10)} ${d}`, - color, - ) - } else { - log(namespace, JSON.stringify(msg), color) - } -} - /** * Helpers to build OSU and Final firmware params */ diff --git a/src/helpers/devices/getIsGenuine.js b/src/helpers/devices/getIsGenuine.js index 13bfdffc..9af44aba 100644 --- a/src/helpers/devices/getIsGenuine.js +++ b/src/helpers/devices/getIsGenuine.js @@ -5,7 +5,7 @@ import { createSocketDialog } from 'helpers/common' export default async ( transport: Transport<*>, { targetId }: { targetId: string | number }, -): Promise<*> => +): Promise => process.env.SKIP_GENUINE > 0 ? new Promise(resolve => setTimeout(() => resolve('0000'), 1000)) : createSocketDialog(transport, '/genuine', { targetId }, true) diff --git a/src/helpers/socket.js b/src/helpers/socket.js new file mode 100644 index 00000000..1204b042 --- /dev/null +++ b/src/helpers/socket.js @@ -0,0 +1,128 @@ +// @flow + +import invariant from 'invariant' +import logger from 'logger' +import Websocket from 'ws' +import type Transport from '@ledgerhq/hw-transport' +import { Observable } from 'rxjs' +import createCustomErrorClass from './createCustomErrorClass' + +const WebsocketConnectionError = createCustomErrorClass('WebsocketConnectionError') +const WebsocketConnectionFailed = createCustomErrorClass('WebsocketConnectionFailed') +const DeviceSocketFail = createCustomErrorClass('DeviceSocketFail') +const DeviceSocketNoBulkStatus = createCustomErrorClass('DeviceSocketNoBulkStatus') +const DeviceSocketNoHandler = createCustomErrorClass('DeviceSocketNoHandler') + +/** + * use Ledger WebSocket API to exchange data with the device + * Returns an Observable of the final result + */ +export const createDeviceSocket = (transport: Transport<*>, url: string) => + Observable.create(o => { + let ws + let lastMessage: ?string + + try { + ws = new Websocket(url) + } catch (err) { + o.error(new WebsocketConnectionFailed(err.message)) + return () => {} + } + invariant(ws, 'websocket is available') + + ws.on('open', () => { + logger.websocket('OPENED', url) + }) + + ws.on('error', e => { + logger.websocket('ERROR', e) + o.error(new WebsocketConnectionError(e.message)) + }) + + ws.on('close', () => { + logger.websocket('CLOSE') + o.next(lastMessage || '') + o.complete() + }) + + const send = (nonce, response, data) => { + const msg = { + nonce, + response, + data, + } + logger.websocket('SEND', msg) + const strMsg = JSON.stringify(msg) + ws.send(strMsg) + } + + const handlers = { + exchange: async input => { + const { data, nonce } = input + const r: Buffer = await transport.exchange(Buffer.from(data, 'hex')) + const status = r.slice(r.length - 2) + const buffer = r.slice(0, r.length - 2) + const strStatus = status.toString('hex') + send(nonce, strStatus === '9000' ? 'success' : 'error', buffer.toString('hex')) + }, + + bulk: async input => { + const { data, nonce } = input + + // Execute all apdus and collect last status + let lastStatus = null + for (const apdu of data) { + const r: Buffer = await transport.exchange(Buffer.from(apdu, 'hex')) + lastStatus = r.slice(r.length - 2) + } + if (!lastStatus) { + throw new DeviceSocketNoBulkStatus() + } + + const strStatus = lastStatus.toString('hex') + + send( + nonce, + strStatus === '9000' ? 'success' : 'error', + strStatus === '9000' ? '' : strStatus, + ) + }, + + success: msg => { + lastMessage = msg.data || msg.result + ws.close() + }, + + error: msg => { + logger.websocket('ERROR', msg.data) + throw new DeviceSocketFail(msg.data) + }, + } + + const stackMessage = async rawMsg => { + try { + const msg = JSON.parse(rawMsg) + if (!(msg.query in handlers)) { + throw new DeviceSocketNoHandler(`Cannot handle msg of type ${msg.query}`, { + query: msg.query, + }) + } + logger.websocket('RECEIVE', msg) + await handlers[msg.query](msg) + } catch (err) { + logger.websocket('ERROR', err.toString()) + o.error(err) + } + } + + ws.on('message', async rawMsg => { + stackMessage(rawMsg) + }) + + return () => { + if (ws.readyState === 1) { + lastMessage = null + ws.close() + } + } + }) diff --git a/src/logger.js b/src/logger.js index e9f47e1a..b8fe6ca8 100644 --- a/src/logger.js +++ b/src/logger.js @@ -52,6 +52,7 @@ const logDb = !__DEV__ || process.env.DEBUG_DB const logRedux = !__DEV__ || process.env.DEBUG_ACTION const logTabkey = !__DEV__ || process.env.DEBUG_TAB_KEY const logLibcore = !__DEV__ || process.env.DEBUG_LIBCORE +const logWS = !__DEV__ || process.env.DEBUG_WS export default { onCmd: (type: string, id: string, spentTime: number, data?: any) => { @@ -104,6 +105,13 @@ export default { addLog('keydown', msg) }, + websocket: (type: string, msg: *) => { + if (logWS) { + console.log(`~ ${type}:`, msg) + } + addLog('ws', `~ ${type}`, msg) + }, + libcore: (level: string, msg: string) => { if (logLibcore) { console.log(`🛠 ${level}: ${msg}`) diff --git a/static/i18n/en/errors.yml b/static/i18n/en/errors.yml index 7b6596f8..456fc91a 100644 --- a/static/i18n/en/errors.yml +++ b/static/i18n/en/errors.yml @@ -12,3 +12,8 @@ LedgerAPIError: 'A problem occurred with Ledger API. Please try again later. (HT NetworkDown: 'Your internet connection seems down. Please try again later.' NoAddressesFound: 'No accounts found' UserRefusedOnDevice: Transaction have been aborted +WebsocketConnectionError: An error occurred with the socket connection +WebsocketConnectionFailed: Failed to establish a socket connection +DeviceSocketFail: Device socket failure +DeviceSocketNoBulkStatus: Device socket failure (bulk) +DeviceSocketNoHandler: Device socket failure (handler {{query}})