Browse Source

Improve socket implementation

master
Gaëtan Renaudeau 7 years ago
parent
commit
de9e76a70e
  1. 2
      src/commands/getIsGenuine.js
  2. 2
      src/helpers/apps/installApp.js
  3. 2
      src/helpers/apps/uninstallApp.js
  4. 160
      src/helpers/common.js
  5. 2
      src/helpers/devices/getIsGenuine.js
  6. 128
      src/helpers/socket.js
  7. 8
      src/logger.js
  8. 5
      static/i18n/en/errors.yml

2
src/commands/getIsGenuine.js

@ -6,7 +6,7 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import getIsGenuine from 'helpers/devices/getIsGenuine' import getIsGenuine from 'helpers/devices/getIsGenuine'
import { withDevice } from 'helpers/deviceAccess' import { withDevice } from 'helpers/deviceAccess'
type Input = * type Input = * // FIXME !
type Result = string type Result = string
const cmd: Command<Input, Result> = createCommand('getIsGenuine', ({ devicePath, targetId }) => const cmd: Command<Input, Result> = createCommand('getIsGenuine', ({ devicePath, targetId }) =>

2
src/helpers/apps/installApp.js

@ -11,6 +11,6 @@ import type { LedgerScriptParams } from 'helpers/common'
export default async function installApp( export default async function installApp(
transport: Transport<*>, transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams }, { appParams }: { appParams: LedgerScriptParams },
): Promise<void> { ): Promise<*> {
return createSocketDialog(transport, '/install', appParams) return createSocketDialog(transport, '/install', appParams)
} }

2
src/helpers/apps/uninstallApp.js

@ -11,7 +11,7 @@ import type { LedgerScriptParams } from 'helpers/common'
export default async function uninstallApp( export default async function uninstallApp(
transport: Transport<*>, transport: Transport<*>,
{ appParams }: { appParams: LedgerScriptParams }, { appParams }: { appParams: LedgerScriptParams },
): Promise<void> { ): Promise<*> {
const params = { const params = {
...appParams, ...appParams,
firmware: appParams.delete, firmware: appParams.delete,

160
src/helpers/common.js

@ -1,24 +1,13 @@
// @flow // @flow
import chalk from 'chalk' // FIXME remove this file! 'helpers/common.js' RLY? :P
import Websocket from 'ws'
import qs from 'qs' import qs from 'qs'
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import { createDeviceSocket } from './socket'
import { BASE_SOCKET_URL, APDUS, MANAGER_API_URL } from './constants' 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 = { export type LedgerScriptParams = {
firmware?: string, firmware?: string,
firmwareKey?: string, firmwareKey?: string,
@ -35,59 +24,6 @@ export async function getMemInfos(transport: Transport<*>): Promise<Object> {
return createSocketDialog(transport, '/get-mem-infos', { targetId, perso: 'perso_11' }) 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<void> {
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 * Open socket connection with firmware api, and init a dialog
* with the device * with the device
@ -97,56 +33,10 @@ export async function createSocketDialog(
endpoint: string, endpoint: string,
params: LedgerScriptParams, params: LedgerScriptParams,
managerUrl: boolean = false, managerUrl: boolean = false,
) { ): Promise<string> {
return new Promise(async (resolve, reject) => { console.warn('DEPRECATED createSocketDialog: use createDeviceSocket') // eslint-disable-line
try { const url = `${managerUrl ? MANAGER_API_URL : BASE_SOCKET_URL}${endpoint}?${qs.stringify(params)}`
let lastData return createDeviceSocket(transport, url).toPromise()
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)
}
})
} }
/** /**
@ -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 * Helpers to build OSU and Final firmware params
*/ */

2
src/helpers/devices/getIsGenuine.js

@ -5,7 +5,7 @@ import { createSocketDialog } from 'helpers/common'
export default async ( export default async (
transport: Transport<*>, transport: Transport<*>,
{ targetId }: { targetId: string | number }, { targetId }: { targetId: string | number },
): Promise<*> => ): Promise<string> =>
process.env.SKIP_GENUINE > 0 process.env.SKIP_GENUINE > 0
? new Promise(resolve => setTimeout(() => resolve('0000'), 1000)) ? new Promise(resolve => setTimeout(() => resolve('0000'), 1000))
: createSocketDialog(transport, '/genuine', { targetId }, true) : createSocketDialog(transport, '/genuine', { targetId }, true)

128
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()
}
}
})

8
src/logger.js

@ -52,6 +52,7 @@ const logDb = !__DEV__ || process.env.DEBUG_DB
const logRedux = !__DEV__ || process.env.DEBUG_ACTION const logRedux = !__DEV__ || process.env.DEBUG_ACTION
const logTabkey = !__DEV__ || process.env.DEBUG_TAB_KEY const logTabkey = !__DEV__ || process.env.DEBUG_TAB_KEY
const logLibcore = !__DEV__ || process.env.DEBUG_LIBCORE const logLibcore = !__DEV__ || process.env.DEBUG_LIBCORE
const logWS = !__DEV__ || process.env.DEBUG_WS
export default { export default {
onCmd: (type: string, id: string, spentTime: number, data?: any) => { onCmd: (type: string, id: string, spentTime: number, data?: any) => {
@ -104,6 +105,13 @@ export default {
addLog('keydown', msg) addLog('keydown', msg)
}, },
websocket: (type: string, msg: *) => {
if (logWS) {
console.log(`~ ${type}:`, msg)
}
addLog('ws', `~ ${type}`, msg)
},
libcore: (level: string, msg: string) => { libcore: (level: string, msg: string) => {
if (logLibcore) { if (logLibcore) {
console.log(`🛠 ${level}: ${msg}`) console.log(`🛠 ${level}: ${msg}`)

5
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.' NetworkDown: 'Your internet connection seems down. Please try again later.'
NoAddressesFound: 'No accounts found' NoAddressesFound: 'No accounts found'
UserRefusedOnDevice: Transaction have been aborted 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}})

Loading…
Cancel
Save