UNPKG

@trezor/transport

Version:

Low level library facilitating protocol buffers based communication with Trezor devices

272 lines 10.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BridgeTransport = void 0; const tslib_1 = require("tslib"); const protocol_1 = require("@trezor/protocol"); const abstract_1 = require("./abstract"); const constants_1 = require("../constants"); const ERRORS = tslib_1.__importStar(require("../errors")); const receive_1 = require("../thp/receive"); const bridgeApiCall_1 = require("../utils/bridgeApiCall"); const bridgeApiResult = tslib_1.__importStar(require("../utils/bridgeApiResult")); const bridgeProtocolMessage_1 = require("../utils/bridgeProtocolMessage"); const receive_2 = require("../utils/receive"); const send_1 = require("../utils/send"); const DEFAULT_URL = 'http://127.0.0.1:21325'; class BridgeTransport extends abstract_1.AbstractTransport { useProtocolMessages = false; url; name = 'BridgeTransport'; apiType = 'usb'; constructor(params) { const { url = DEFAULT_URL, ...rest } = params || {}; super(rest); this.url = url; } ping({ signal } = {}) { return this.scheduleAction(signal => this.post('/', { signal }), { signal }) .then(({ success }) => success) .catch(() => false); } init({ signal } = {}) { return this.scheduleAction(async (signal) => { const response = await this.post('/', { signal, }); if (!response.success) { return response; } this.version = response.payload.version; if (this.version.startsWith('3')) { this.isOutdated = false; } else { this.isOutdated = !['2.0.27', '2.0.33'].includes(this.version); } this.useProtocolMessages = !!response.payload.protocolMessages; this.stopped = false; return this.success(undefined); }, { signal }); } listen() { if (this.listening) { return this.error({ error: ERRORS.ALREADY_LISTENING }); } this.listening = true; this.listenLoop(); return this.success(undefined); } async listenLoop() { while (!this.stopped) { const response = await this.post('/listen', { body: this.descriptors, signal: this.abortController.signal, }); if (!response.success) { this.emit(constants_1.TRANSPORT.ERROR, response.error); } else { this.handleDescriptorsChange(response.payload); } } } enumerate({ signal } = {}) { return this.scheduleAction(signal => this.post('/enumerate', { signal }), { signal }); } acquire({ input, signal }) { return this.scheduleAction(async (signal) => { const response = await this.post('/acquire', { params: `${input.path}/${input.previous ?? 'null'}`, signal, body: { sessionOwner: this.id, }, }); return response; }, { signal }, [ERRORS.DEVICE_DISCONNECTED_DURING_ACTION, ERRORS.SESSION_WRONG_PREVIOUS]); } release({ path: _, session, signal }) { return this.scheduleAction(async (signal) => { const response = await this.post('/release', { params: session, signal, }); return response.success ? this.success(null) : response; }, { signal }); } releaseSync(session) { if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) { navigator.sendBeacon(`${this.url}/release/${session}?beacon=1`); } else { this.post('/release', { params: session }); } } releaseDevice() { return Promise.resolve(this.success(undefined)); } getProtocol(customProtocol) { if (!this.useProtocolMessages) { return protocol_1.bridge; } return customProtocol || protocol_1.v1; } getRequestBody(body, protocol, thpState) { return (0, bridgeProtocolMessage_1.createProtocolMessage)(body, this.useProtocolMessages ? protocol : undefined, thpState?.serialize()); } call({ session, name, data, protocol: customProtocol, thpState, signal, timeout, }) { return this.scheduleAction(async (signal) => { const protocol = this.getProtocol(customProtocol); const bytes = (0, send_1.buildMessage)({ messages: this.messages, name, data, protocol, thpState, }); const response = await this.post(`/call`, { params: session, body: this.getRequestBody(bytes, protocol, thpState), signal, }); if (!response.success) { return response; } const respBytes = Buffer.from(response.payload.data, 'hex'); if (protocol.name === 'v2') { thpState?.sync('send', name); const message = (0, receive_1.parseThpMessage)({ decoded: protocol.decode(respBytes), messages: this.messages, thpState, }); thpState?.sync('recv', message.type); return this.success(message); } return (0, receive_2.receiveAndParse)(this.messages, () => Promise.resolve(this.success(respBytes)), protocol); }, { signal, timeout }); } send({ session, name, data, protocol: customProtocol, thpState, signal, timeout, }) { return this.scheduleAction(async (signal) => { const protocol = this.getProtocol(customProtocol); const bytes = (0, send_1.buildMessage)({ messages: this.messages, name, data, protocol, thpState, }); const response = await this.post('/post', { params: session, body: this.getRequestBody(bytes, protocol, thpState), signal, }); if (!response.success) { return response; } if (protocol.name === 'v2') { thpState?.sync('send', name); } return this.success(undefined); }, { signal, timeout }); } receive({ session, protocol: customProtocol, thpState, signal, timeout, }) { return this.scheduleAction(async (signal) => { const protocol = this.getProtocol(customProtocol); const response = await this.post('/read', { params: session, body: this.getRequestBody(Buffer.alloc(0), protocol, thpState), signal, }); if (!response.success) { return response; } const respBytes = Buffer.from(response.payload.data, 'hex'); if (protocol.name === 'v2') { const message = (0, receive_1.parseThpMessage)({ decoded: protocol.decode(respBytes), messages: this.messages, thpState, }); thpState?.sync('recv', message.type); return this.success(message); } return (0, receive_2.receiveAndParse)(this.messages, () => Promise.resolve(this.success(respBytes)), protocol); }, { signal, timeout }); } async post(endpoint, options) { const response = await (0, bridgeApiCall_1.bridgeApiCall)({ ...options, method: 'POST', url: `${this.url + endpoint}${options?.params ? `/${options.params}` : ''}`, skipContentTypeHeader: true, }); if (!response.success) { if (response.error === ERRORS.UNEXPECTED_ERROR) { return this.unknownError(response.error); } if (response.error === ERRORS.HTTP_ERROR) { return this.error({ error: response.error }); } switch (endpoint) { case '/': return this.unknownError(response.error); case '/acquire': return this.unknownError(response.error, [ ERRORS.SESSION_WRONG_PREVIOUS, ERRORS.DEVICE_NOT_FOUND, ERRORS.INTERFACE_UNABLE_TO_OPEN_DEVICE, ERRORS.DEVICE_DISCONNECTED_DURING_ACTION, ERRORS.LIBUSB_ERROR_ACCESS, ]); case '/call': case '/read': case '/post': return this.unknownError(response.error, [ ERRORS.SESSION_NOT_FOUND, ERRORS.DEVICE_DISCONNECTED_DURING_ACTION, ERRORS.OTHER_CALL_IN_PROGRESS, ERRORS.INTERFACE_DATA_TRANSFER, protocol_1.PROTOCOL_MALFORMED, ]); case '/enumerate': case '/listen': return this.unknownError(response.error); case '/release': return this.unknownError(response.error, [ ERRORS.SESSION_NOT_FOUND, ERRORS.DEVICE_DISCONNECTED_DURING_ACTION, ]); default: return this.error({ error: ERRORS.WRONG_RESULT_TYPE, message: 'just for type safety, should never happen', }); } } switch (endpoint) { case '/': return bridgeApiResult.info(response.payload); case '/acquire': return bridgeApiResult.acquire(response.payload); case '/read': case '/call': return bridgeApiResult.call(response.payload); case '/post': return bridgeApiResult.post(response.payload); case '/enumerate': case '/listen': return bridgeApiResult.devices(response.payload); case '/release': return bridgeApiResult.empty(response.payload); default: return this.error({ error: ERRORS.WRONG_RESULT_TYPE, message: 'just for type safety, should never happen', }); } } } exports.BridgeTransport = BridgeTransport; //# sourceMappingURL=bridge.js.map