@trezor/transport
Version:
Low level library facilitating protocol buffers based communication with Trezor devices
317 lines (316 loc) • 9.23 kB
JavaScript
"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 ping_1 = require("../pinger/ping");
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';
const DEFAULT_PORT = 21325;
class BridgeTransport extends abstract_1.AbstractTransport {
useProtocolMessages = false;
url;
name = 'BridgeTransport';
constructor(params) {
const {
port = DEFAULT_PORT,
...rest
} = params || {};
super(rest);
this.url = `${DEFAULT_URL}:${port}`;
}
ping(_ = {}) {
return (0, ping_1.ping)(`${this.url}/`).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 = true;
}
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 prevNonce = thpState?.sendNonce;
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') {
if (prevNonce === thpState?.sendNonce) {
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