UNPKG

7.14 kBJavaScriptView Raw
1/* @flow */
2
3import {patch} from './protobuf/monkey_patch';
4patch();
5
6import {create as createDefered} from '../defered';
7import {parseConfigure} from './protobuf/parse_protocol';
8import {verifyHexBin} from './verify';
9import {buildAndSend} from './send';
10import {receiveAndParse} from './receive';
11
12// eslint-disable-next-line quotes
13const stringify = require('json-stable-stringify');
14
15import type {LowlevelTransportPlugin} from './plugin';
16import type {Defered} from '../defered';
17import type {Messages} from './protobuf/messages';
18import type {MessageFromTrezor, TrezorDeviceInfoWithSession, AcquireInput} from '../transport';
19
20import {debugInOut} from '../debug-decorator';
21
22function stableStringify(devices: ?Array<TrezorDeviceInfoWithSession>): string {
23 if (devices == null) {
24 return `null`;
25 }
26
27 const pureDevices = devices.map(device => {
28 const path = device.path;
29 const session = device.session == null ? null : device.session;
30 return {path, session};
31 });
32
33 return stringify(pureDevices);
34}
35
36function compare(a: TrezorDeviceInfoWithSession, b: TrezorDeviceInfoWithSession): number {
37 if (!isNaN(a.path)) {
38 return parseInt(a.path) - parseInt(b.path);
39 } else {
40 return a.path < a.path ? -1 : (a.path > a.path ? 1 : 0);
41 }
42}
43
44function timeoutPromise(delay: number): Promise<void> {
45 return new Promise((resolve) => {
46 setTimeout(() => resolve(), delay);
47 });
48}
49
50const ITER_MAX = 60;
51const ITER_DELAY = 500;
52
53export default class LowlevelTransport {
54 name: string = `LowlevelTransport`;
55
56 plugin: LowlevelTransportPlugin;
57 _lock: Promise<any> = Promise.resolve();
58 debug: boolean = false;
59
60 // path => promise rejecting on release
61 deferedOnRelease: {[session: string]: Defered<void>} = {};
62
63 // path => session
64 connections: {[path: string]: string} = {};
65
66 // session => path
67 reverse: {[session: string]: string} = {};
68
69 _messages: ?Messages;
70 version: string;
71 configured: boolean = false;
72
73 constructor(plugin: LowlevelTransportPlugin) {
74 this.plugin = plugin;
75 this.version = plugin.version;
76 }
77
78 lock<X>(fn: () => (Promise<X>)): Promise<X> {
79 const res = this._lock.then(() => fn());
80 this._lock = res.catch(() => {});
81 return res;
82 }
83
84 @debugInOut
85 enumerate(): Promise<Array<TrezorDeviceInfoWithSession>> {
86 return this._silentEnumerate();
87 }
88
89 _silentEnumerate(): Promise<Array<TrezorDeviceInfoWithSession>> {
90 return this.lock(async (): Promise<Array<TrezorDeviceInfoWithSession>> => {
91 const devices = await this.plugin.enumerate();
92 const devicesWithSessions = devices.map(device => {
93 return {
94 ...device,
95 session: this.connections[device.path],
96 };
97 });
98 this._releaseDisconnected(devicesWithSessions);
99 return devicesWithSessions.sort(compare);
100 });
101 }
102
103 _releaseDisconnected(devices: Array<TrezorDeviceInfoWithSession>) {
104 const connected: {[path: string]: boolean} = {};
105 devices.forEach(device => {
106 connected[device.path] = true;
107 });
108 Object.keys(this.connections).forEach(path => {
109 if (connected[path] == null) {
110 if (this.connections[path] != null) {
111 this._releaseCleanup(this.connections[path]);
112 }
113 }
114 });
115 }
116
117 _lastStringified: string = ``;
118
119 @debugInOut
120 async listen(old: ?Array<TrezorDeviceInfoWithSession>): Promise<Array<TrezorDeviceInfoWithSession>> {
121 const oldStringified = stableStringify(old);
122 const last = old == null ? this._lastStringified : oldStringified;
123 return this._runIter(0, last);
124 }
125
126 async _runIter(iteration: number, oldStringified: string): Promise<Array<TrezorDeviceInfoWithSession>> {
127 const devices = await this._silentEnumerate();
128 const stringified = stableStringify(devices);
129 if ((stringified !== oldStringified) || (iteration === ITER_MAX)) {
130 this._lastStringified = stringified;
131 return devices;
132 }
133 await timeoutPromise(ITER_DELAY);
134 return this._runIter(iteration + 1, stringified);
135 }
136
137 async _checkAndReleaseBeforeAcquire(input: AcquireInput): Promise<void> {
138 const realPrevious = this.connections[input.path];
139 if (input.checkPrevious) {
140 let error = false;
141 if (realPrevious == null) {
142 error = (input.previous != null);
143 } else {
144 error = (input.previous !== realPrevious);
145 }
146 if (error) {
147 throw new Error(`wrong previous session`);
148 }
149 }
150 if (realPrevious != null) {
151 await this._realRelease(input.path, realPrevious);
152 }
153 }
154
155 @debugInOut
156 async acquire(input: AcquireInput): Promise<string> {
157 return this.lock(async (): Promise<string> => {
158 await this._checkAndReleaseBeforeAcquire(input);
159 const session = await this.plugin.connect(input.path);
160 this.connections[input.path] = session;
161 this.reverse[session] = input.path;
162 this.deferedOnRelease[session] = createDefered();
163 return session;
164 });
165 }
166
167 @debugInOut
168 async release(session: string): Promise<void> {
169 const path = this.reverse[session];
170 if (path == null) {
171 throw new Error(`Trying to double release.`);
172 }
173 return this.lock(() => this._realRelease(path, session));
174 }
175
176 async _realRelease(path:string, session: string): Promise<void> {
177 await this.plugin.disconnect(path, session);
178 this._releaseCleanup(session);
179 }
180
181 _releaseCleanup(session: string) {
182 const path: string = this.reverse[session];
183 delete this.reverse[session];
184 delete this.connections[path];
185 this.deferedOnRelease[session].reject(new Error(`Device released or disconnected`));
186 delete this.deferedOnRelease[session];
187 return;
188 }
189
190 @debugInOut
191 async configure(signedData: string): Promise<void> {
192 const buffer = verifyHexBin(signedData);
193 const messages = parseConfigure(buffer);
194 this._messages = messages;
195 this.configured = true;
196 }
197
198 _sendLowlevel(session: string): (data: ArrayBuffer) => Promise<void> {
199 const path: string = this.reverse[session];
200 return (data) => this.plugin.send(path, session, data);
201 }
202
203 _receiveLowlevel(session: string): () => Promise<ArrayBuffer> {
204 const path: string = this.reverse[session];
205 return () => this.plugin.receive(path, session);
206 }
207
208 @debugInOut
209 async call(session: string, name: string, data: Object): Promise<MessageFromTrezor> {
210 if (this._messages == null) {
211 throw new Error(`Transport not configured.`);
212 }
213 if (this.reverse[session] == null) {
214 throw new Error(`Trying to use device after release.`);
215 }
216
217 const messages = this._messages;
218
219 return this.lock(async (): Promise<MessageFromTrezor> => {
220 const resPromise: Promise<MessageFromTrezor> = (async () => {
221 await buildAndSend(messages, this._sendLowlevel(session), name, data);
222 const message = await receiveAndParse(messages, this._receiveLowlevel(session));
223 return message;
224 })();
225
226 return Promise.race([this.deferedOnRelease[session].rejectingPromise, resPromise]);
227 });
228 }
229
230 @debugInOut
231 async init(debug: ?boolean): Promise<void> {
232 this.debug = !!debug;
233 return this.plugin.init(debug);
234 }
235}