You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
128 lines
3.5 KiB
128 lines
3.5 KiB
// @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()
|
|
}
|
|
}
|
|
})
|
|
|