@tdxvolt/volt-client-grpc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/lib/index.cjs +1883 -0
- package/package.json +50 -0
- package/protobuf/README.md +4 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite.proto +43 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite_database_api.proto +105 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite_server_api.proto +42 -0
- package/protobuf/tdx/volt_api/ssi/v1/ssi_api.proto +37 -0
- package/protobuf/tdx/volt_api/sync/v1/sync.proto +14 -0
- package/protobuf/tdx/volt_api/tunnel/v1/proxy_api.proto +9 -0
- package/protobuf/tdx/volt_api/tunnel/v1/tunnel_api.proto +77 -0
- package/protobuf/tdx/volt_api/volt/v1/discovery_api.proto +39 -0
- package/protobuf/tdx/volt_api/volt/v1/file.proto +17 -0
- package/protobuf/tdx/volt_api/volt/v1/file_api.proto +117 -0
- package/protobuf/tdx/volt_api/volt/v1/remote.proto +94 -0
- package/protobuf/tdx/volt_api/volt/v1/status.proto +15 -0
- package/protobuf/tdx/volt_api/volt/v1/volt.proto +563 -0
- package/protobuf/tdx/volt_api/volt/v1/volt_api.proto +943 -0
- package/protobuf/tdx/volt_api/volt/v1/wire_api.proto +50 -0
- package/src/constants.js +75 -0
- package/src/grpc-call.js +254 -0
- package/src/grpc-utils.js +197 -0
- package/src/index.js +7 -0
- package/src/proto-utils.js +81 -0
- package/src/tunnel.pb.js +0 -0
- package/src/utils.js +105 -0
- package/src/volt-client-internal.js +288 -0
- package/src/volt-client.js +491 -0
- package/src/volt-connection.js +157 -0
- package/src/volt-crypto.js +213 -0
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,1883 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var lodash = require('lodash');
|
|
6
|
+
var ip = require('ip');
|
|
7
|
+
var forge = require('node-forge');
|
|
8
|
+
var bs58 = require('bs58');
|
|
9
|
+
var debug = require('debug');
|
|
10
|
+
var EventEmitter = require('events');
|
|
11
|
+
var fs = require('fs');
|
|
12
|
+
var jwt = require('jsonwebtoken');
|
|
13
|
+
var crypto = require('crypto');
|
|
14
|
+
var path = require('path');
|
|
15
|
+
var protoLoader = require('@grpc/proto-loader');
|
|
16
|
+
var url = require('url');
|
|
17
|
+
var bent = require('bent');
|
|
18
|
+
var uuid = require('uuid');
|
|
19
|
+
|
|
20
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
21
|
+
|
|
22
|
+
var lodash__default = /*#__PURE__*/_interopDefaultLegacy(lodash);
|
|
23
|
+
var ip__default = /*#__PURE__*/_interopDefaultLegacy(ip);
|
|
24
|
+
var forge__default = /*#__PURE__*/_interopDefaultLegacy(forge);
|
|
25
|
+
var bs58__default = /*#__PURE__*/_interopDefaultLegacy(bs58);
|
|
26
|
+
var debug__default = /*#__PURE__*/_interopDefaultLegacy(debug);
|
|
27
|
+
var EventEmitter__default = /*#__PURE__*/_interopDefaultLegacy(EventEmitter);
|
|
28
|
+
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
29
|
+
var jwt__default = /*#__PURE__*/_interopDefaultLegacy(jwt);
|
|
30
|
+
var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
|
|
31
|
+
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
32
|
+
var protoLoader__default = /*#__PURE__*/_interopDefaultLegacy(protoLoader);
|
|
33
|
+
var bent__default = /*#__PURE__*/_interopDefaultLegacy(bent);
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line import/prefer-default-export
|
|
36
|
+
const constants = {
|
|
37
|
+
authTokenName: "tdx-token",
|
|
38
|
+
collectionOperation: {
|
|
39
|
+
add: "ADD",
|
|
40
|
+
remove: "REMOVE",
|
|
41
|
+
update: "UPDATE",
|
|
42
|
+
},
|
|
43
|
+
crypto: {
|
|
44
|
+
subjectDefaults: [
|
|
45
|
+
{
|
|
46
|
+
name: "countryName",
|
|
47
|
+
value: "GB",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "localityName",
|
|
51
|
+
value: "Southampton",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "organizationName",
|
|
55
|
+
value: "nquiringminds Ltd",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
userIdOID: "0.9.2342.19200300.100.1.1",
|
|
59
|
+
},
|
|
60
|
+
defaultDIDHostName: "volt.tdxcloud.com",
|
|
61
|
+
didServiceType: {
|
|
62
|
+
voltConfig: "tdx-volt-config",
|
|
63
|
+
},
|
|
64
|
+
identityType: {
|
|
65
|
+
did: "did",
|
|
66
|
+
volt: "volt",
|
|
67
|
+
},
|
|
68
|
+
onlineStatus: {
|
|
69
|
+
offline: "offline",
|
|
70
|
+
online: "online",
|
|
71
|
+
unknown: "unknown",
|
|
72
|
+
},
|
|
73
|
+
publicKeyPrefix: "-----BEGIN PUBLIC KEY-----",
|
|
74
|
+
privateKeyPrefix: "-----BEGIN RSA PRIVATE KEY-----",
|
|
75
|
+
privateEncryptedKeyPrefix: "-----BEGIN ENCRYPTED PRIVATE KEY-----",
|
|
76
|
+
resource: {
|
|
77
|
+
identity: "ident",
|
|
78
|
+
identityView: "ident-view",
|
|
79
|
+
proxy: "proxy",
|
|
80
|
+
proxyView: "proxy-view",
|
|
81
|
+
vc: "vc",
|
|
82
|
+
vcView: "vc-view",
|
|
83
|
+
},
|
|
84
|
+
voltStatus: {
|
|
85
|
+
pending: "",
|
|
86
|
+
online: "online",
|
|
87
|
+
offline: "offline",
|
|
88
|
+
deleted: "deleted",
|
|
89
|
+
revoked: "revoked",
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* These top-level service types should map to a protobuf definition
|
|
93
|
+
* in @tdxvolt/volt-proto.
|
|
94
|
+
*/
|
|
95
|
+
serviceType: {
|
|
96
|
+
fileAPI: "tdx.volt_api.volt.v1.FileAPI",
|
|
97
|
+
protocolBridgeDataStream: "tdx.volt_api.protocol_bridge.v1.ProtocolBridgeStreamAPI",
|
|
98
|
+
sqliteServerAPI: "tdx.volt_api.data.v1.SqliteServerAPI",
|
|
99
|
+
ssiAPI: "tdx.volt_api.ssi.v1.SsiAPI",
|
|
100
|
+
tunnelAPI: "tdx.volt_api.tunnel.v1.TunnelAPI",
|
|
101
|
+
voltAPI: "tdx.volt_api.volt.v1.VoltAPI",
|
|
102
|
+
wireAPI: "tdx.volt_api.volt.v1.WireAPI",
|
|
103
|
+
},
|
|
104
|
+
tunnelInvokeMethod: "/tdx.volt_api.volt.v1.VoltAPI/Invoke",
|
|
105
|
+
tunnelPingInterval: 10000,
|
|
106
|
+
tunnelPingTimeout: 2500,
|
|
107
|
+
tunnelReconnectAfter: 30000,
|
|
108
|
+
tunnelTimeoutInterval: 10000 * 2, // This must always be greater than `tunnelPingInterval`.
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const {pki: pki$2} = forge__default["default"];
|
|
112
|
+
|
|
113
|
+
const createSecureContextOptions = (cryptoOptions) => {
|
|
114
|
+
const tlsOptions = {
|
|
115
|
+
cert: Buffer.from(cryptoOptions.cert),
|
|
116
|
+
ca: Buffer.from(cryptoOptions.ca),
|
|
117
|
+
key: Buffer.from(cryptoOptions.key),
|
|
118
|
+
};
|
|
119
|
+
return tlsOptions;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const getServiceAddress = (mdnsService) => {
|
|
123
|
+
let address;
|
|
124
|
+
|
|
125
|
+
if (mdnsService.txtRecord && mdnsService.txtRecord.ip) {
|
|
126
|
+
// Use the advertised IP address if present.
|
|
127
|
+
address = `${mdnsService.txtRecord.ip}:${mdnsService.port}`;
|
|
128
|
+
} else {
|
|
129
|
+
// Find a valid ipv4 address.
|
|
130
|
+
const ipv4 = lodash__default["default"].find(mdnsService.addresses, (addy) => ip__default["default"].isV4Format(addy));
|
|
131
|
+
const serviceDetails = {
|
|
132
|
+
host: ipv4,
|
|
133
|
+
port: mdnsService.port,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
address = `${serviceDetails.host}:${serviceDetails.port}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return address;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const privateKeyToPem = (keyPair) => {
|
|
143
|
+
return pki$2.privateKeyToPem(keyPair.privateKey);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const publicKeyToPem = (keyPair) => {
|
|
147
|
+
return pki$2.publicKeyToPem(keyPair.publicKey);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const deserialisePEM = (pem) => {
|
|
151
|
+
return pem.replace(/\\r\\n/g, "\r\n");
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const removeCarriageReturn = (inp) => {
|
|
155
|
+
return inp.replace(/\r\n/g, "\n");
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const createBase58Hash = (inp) => {
|
|
159
|
+
const md = forge__default["default"].md.sha256.create();
|
|
160
|
+
md.update(inp);
|
|
161
|
+
const hashBuf = Buffer.from(md.digest().getBytes(), "binary");
|
|
162
|
+
return bs58__default["default"].encode(hashBuf);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const createPublicKeyFingerprint = (key) => {
|
|
166
|
+
// Remove carriage returns before creating hash to bring into line with openSSL / volt format.
|
|
167
|
+
return createBase58Hash(removeCarriageReturn(key));
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const signBase64 = (key, msg) => {
|
|
171
|
+
const md = forge__default["default"].md.sha256.create();
|
|
172
|
+
md.update(msg);
|
|
173
|
+
const signature = key.sign(md);
|
|
174
|
+
return forge__default["default"].util.encode64(signature);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const verifyBase64 = (key, msg, signature) => {
|
|
178
|
+
const md = forge__default["default"].md.sha256.create();
|
|
179
|
+
md.update(msg);
|
|
180
|
+
return key.verify(md.digest().bytes(), forge__default["default"].util.decode64(signature));
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const getDIDResolutionURL = (didIn) => {
|
|
184
|
+
const did = didIn.toLowerCase().split(":");
|
|
185
|
+
if (did.length < 3) {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (did[0] !== "did") {
|
|
190
|
+
return "";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (did[1] !== "tdx") {
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let hostName;
|
|
198
|
+
let didGUID;
|
|
199
|
+
if (did.length === 3) {
|
|
200
|
+
hostName = constants.defaultDIDHostName;
|
|
201
|
+
didGUID = did[2];
|
|
202
|
+
} else {
|
|
203
|
+
hostName = did[2];
|
|
204
|
+
didGUID = did[3];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return `https://${hostName}/api/identity/${didGUID}`;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
var utils = /*#__PURE__*/Object.freeze({
|
|
211
|
+
__proto__: null,
|
|
212
|
+
createSecureContextOptions: createSecureContextOptions,
|
|
213
|
+
getServiceAddress: getServiceAddress,
|
|
214
|
+
privateKeyToPem: privateKeyToPem,
|
|
215
|
+
publicKeyToPem: publicKeyToPem,
|
|
216
|
+
deserialisePEM: deserialisePEM,
|
|
217
|
+
removeCarriageReturn: removeCarriageReturn,
|
|
218
|
+
createBase58Hash: createBase58Hash,
|
|
219
|
+
createPublicKeyFingerprint: createPublicKeyFingerprint,
|
|
220
|
+
signBase64: signBase64,
|
|
221
|
+
verifyBase64: verifyBase64,
|
|
222
|
+
getDIDResolutionURL: getDIDResolutionURL
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/* eslint-disable no-underscore-dangle */
|
|
226
|
+
|
|
227
|
+
const {forEach, isEmpty} = lodash__default["default"];
|
|
228
|
+
|
|
229
|
+
const serialiseNOP = (arg) => {
|
|
230
|
+
return arg;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
function createClient(grpc, serviceDescriptors, address, cryptoOptions, noSerialise, voltId, serviceId) {
|
|
234
|
+
let credentials;
|
|
235
|
+
if (cryptoOptions) {
|
|
236
|
+
const ca = cryptoOptions.tunnel_ca_pem || cryptoOptions.ca;
|
|
237
|
+
const cert = cryptoOptions.cloud_cert || cryptoOptions.cert;
|
|
238
|
+
const {key} = cryptoOptions;
|
|
239
|
+
|
|
240
|
+
if (key && cert) {
|
|
241
|
+
const cryyptoOptions = {ca, cert, key};
|
|
242
|
+
const tlsOptions = createSecureContextOptions(cryyptoOptions);
|
|
243
|
+
credentials = grpc.credentials.createSsl(tlsOptions.ca, tlsOptions.key, tlsOptions.cert);
|
|
244
|
+
} else {
|
|
245
|
+
// Cache has no private key, this implies we are connecting without supplying a client certificate.
|
|
246
|
+
// Simply connect using the CA certificate.
|
|
247
|
+
credentials = grpc.credentials.createSsl(Buffer.from(ca));
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
credentials = grpc.credentials.createInsecure();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Force all methods to appear bi-directional, since we manually handle the call flow (see grpc-call.js).
|
|
254
|
+
forEach(serviceDescriptors, (method) => {
|
|
255
|
+
method.requestStream = true;
|
|
256
|
+
method.responseStream = true;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (noSerialise || voltId) {
|
|
260
|
+
if (voltId && !serviceId) {
|
|
261
|
+
// We must have a service identifier id when tunnelling in order to be able to route the rpc.
|
|
262
|
+
throw new Error("createClient - no serviceId given");
|
|
263
|
+
}
|
|
264
|
+
forEach(serviceDescriptors, (method) => {
|
|
265
|
+
if (voltId) {
|
|
266
|
+
// Prepend the resource id to the method path if tunnelling.
|
|
267
|
+
if (voltId === serviceId) {
|
|
268
|
+
method.path = `/${voltId}${method.path}`;
|
|
269
|
+
} else {
|
|
270
|
+
method.path = `/${voltId}/${serviceId}${method.path}`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (noSerialise) {
|
|
274
|
+
// Save original serialisation methods so we can encrypt/decrypt in the tunnel.
|
|
275
|
+
method.requestSerializeOriginal = method.requestSerialize;
|
|
276
|
+
method.responseDeserializeOriginal = method.responseDeserialize;
|
|
277
|
+
|
|
278
|
+
// Replace default serialisation with NOP, since when tunnelling we encrypt the payload
|
|
279
|
+
// before sending it to the channel, so we want it to be treated as a pure byte array (pass-through).
|
|
280
|
+
method.requestSerialize = serialiseNOP;
|
|
281
|
+
method.responseDeserialize = serialiseNOP;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const SubClass = grpc.makeGenericClientConstructor(serviceDescriptors);
|
|
287
|
+
|
|
288
|
+
return new SubClass(address, credentials);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Implementations should utilise promises => wrap each method as a grpc request/response callback.
|
|
293
|
+
* @param {*} api
|
|
294
|
+
*/
|
|
295
|
+
const grpcWrap = (api) => {
|
|
296
|
+
const grpcRoutes = {};
|
|
297
|
+
|
|
298
|
+
// Don't bother trying to wrap class constructors.
|
|
299
|
+
const ignoreMethods = ["constructor"];
|
|
300
|
+
|
|
301
|
+
// Support class instances and PoJos
|
|
302
|
+
const isClass = api.constructor !== Object;
|
|
303
|
+
const iterate = Object.getOwnPropertyNames(isClass ? api.constructor.prototype : api);
|
|
304
|
+
|
|
305
|
+
// Wrap each method on the api.
|
|
306
|
+
forEach(iterate, (name) => {
|
|
307
|
+
if (!ignoreMethods.includes(name)) {
|
|
308
|
+
const method = api[name];
|
|
309
|
+
grpcRoutes[name] = (call, callback) => {
|
|
310
|
+
// Execute the method in the context of the api.
|
|
311
|
+
const resultPromise = method.call(api, call.request, call);
|
|
312
|
+
|
|
313
|
+
// Enforce promise results.
|
|
314
|
+
if (!resultPromise || !resultPromise.then || !resultPromise.catch) {
|
|
315
|
+
throw new Error(`implementation methods must return a Promise [${name}]`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
resultPromise
|
|
319
|
+
.then((response) => {
|
|
320
|
+
if (callback) {
|
|
321
|
+
callback(null, response);
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
.catch((err) => {
|
|
325
|
+
if (callback) {
|
|
326
|
+
callback(err);
|
|
327
|
+
} else {
|
|
328
|
+
// TODO - verify this => how to send error to stream clients?
|
|
329
|
+
// Destroy stream on error.
|
|
330
|
+
call.destroy(err);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return grpcRoutes;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
class GrpcServer {
|
|
341
|
+
constructor(options) {
|
|
342
|
+
const {grpc, serviceDescriptors, routes, tlsOptions, requireClientCert} = options;
|
|
343
|
+
this._grpc = grpc;
|
|
344
|
+
this._server = new this._grpc.Server();
|
|
345
|
+
|
|
346
|
+
if (tlsOptions) {
|
|
347
|
+
const caList = [];
|
|
348
|
+
const certChains = [];
|
|
349
|
+
[].concat(tlsOptions).forEach((tlsOption) => {
|
|
350
|
+
caList.push(tlsOption.ca);
|
|
351
|
+
certChains.push({
|
|
352
|
+
private_key: tlsOption.key,
|
|
353
|
+
cert_chain: tlsOption.cert,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Create credentials from the volt CA and our own pub/priv key.
|
|
358
|
+
this._credentials = grpc.ServerCredentials.createSsl(
|
|
359
|
+
// The root CAs we trust when verifying client certificates.
|
|
360
|
+
Buffer.concat(caList),
|
|
361
|
+
// The certificate chain(s) we present to the client.
|
|
362
|
+
certChains,
|
|
363
|
+
// Flag indicating if we insist on a client certificate.
|
|
364
|
+
requireClientCert
|
|
365
|
+
);
|
|
366
|
+
} else {
|
|
367
|
+
this._credentials = grpc.ServerCredentials.createInsecure();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (serviceDescriptors && !isEmpty(routes)) {
|
|
371
|
+
// Enforce decoupling of the service implementation.
|
|
372
|
+
const router = grpcWrap(routes);
|
|
373
|
+
this._server.addService(serviceDescriptors, router);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
close(force = false) {
|
|
378
|
+
return new Promise((resolve) => {
|
|
379
|
+
if (force) {
|
|
380
|
+
resolve(this._server.forceShutdown());
|
|
381
|
+
} else {
|
|
382
|
+
this._server.tryShutdown(resolve);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
listen(address) {
|
|
388
|
+
return new Promise((resolve, reject) => {
|
|
389
|
+
// If no port is specified, grpc will return the port number assigned.
|
|
390
|
+
this._server.bindAsync(address, this._credentials, (err, port) => {
|
|
391
|
+
if (err) {
|
|
392
|
+
reject(err);
|
|
393
|
+
} else {
|
|
394
|
+
this._port = port;
|
|
395
|
+
this._server.start();
|
|
396
|
+
resolve(port);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
get server() {
|
|
403
|
+
return this._server;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
get port() {
|
|
407
|
+
return this._port;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function createServer(grpc, serviceDescriptors, routes, cryptoCache, requireClientCert) {
|
|
412
|
+
let tlsOptions;
|
|
413
|
+
if (cryptoCache) {
|
|
414
|
+
tlsOptions = createSecureContextOptions(cryptoCache);
|
|
415
|
+
}
|
|
416
|
+
const serverArgs = {grpc, serviceDescriptors, routes, tlsOptions, requireClientCert};
|
|
417
|
+
return new GrpcServer(serverArgs);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
var grpcUtils = /*#__PURE__*/Object.freeze({
|
|
421
|
+
__proto__: null,
|
|
422
|
+
createClient: createClient,
|
|
423
|
+
createServer: createServer
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
/* eslint-disable no-underscore-dangle */
|
|
427
|
+
|
|
428
|
+
const {pki: pki$1} = forge__default["default"];
|
|
429
|
+
const log$4 = debug__default["default"]("volt-client-grpc:volt-crypto");
|
|
430
|
+
const aesAlgorithm = "aes-256-cbc";
|
|
431
|
+
const rs256Algorithm = "RS256";
|
|
432
|
+
|
|
433
|
+
class VoltCrypto {
|
|
434
|
+
constructor(config, configPath) {
|
|
435
|
+
this._config = config;
|
|
436
|
+
this._voltConfig = config.volt;
|
|
437
|
+
this._configPath = configPath;
|
|
438
|
+
this._persistCache = !!configPath;
|
|
439
|
+
|
|
440
|
+
if (!this._config.crypto) {
|
|
441
|
+
this._cryptoCache = {};
|
|
442
|
+
} else {
|
|
443
|
+
this._cryptoCache = this._config.crypto;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Determine if we're dealing with an encrypted key.
|
|
447
|
+
if (this._cryptoCache.key && this._cryptoCache.key.startsWith(constants.privateEncryptedKeyPrefix)) {
|
|
448
|
+
if (!config.p) {
|
|
449
|
+
throw new Error("encrypted key but no passphrase given");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Need to unencrypt the private key.
|
|
453
|
+
const decrypted = pki$1.decryptRsaPrivateKey(this._cryptoCache.key, config.p);
|
|
454
|
+
if (!decrypted) {
|
|
455
|
+
throw new Error("failed to decrypt key - check passphrase");
|
|
456
|
+
}
|
|
457
|
+
this._cryptoCache.key = pki$1.privateKeyToPem(decrypted);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (this._voltConfig.ca_pem) {
|
|
461
|
+
if (!this._cryptoCache.ca) {
|
|
462
|
+
// Copy the volt signing CA to the crypto cache.
|
|
463
|
+
this._cryptoCache.ca = this._voltConfig.ca_pem;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Pre-cache the target Volt public key by extracting it from the signing certificate.
|
|
467
|
+
const voltCert = forge__default["default"].pki.certificateFromPem(this._voltConfig.ca_pem);
|
|
468
|
+
|
|
469
|
+
// Extract Volt public key (used for encrypting tunnel payloads).
|
|
470
|
+
this._voltPublicKey = forge__default["default"].pki.publicKeyToPem(voltCert.publicKey);
|
|
471
|
+
this._voltFingerprint = createPublicKeyFingerprint(this._voltPublicKey);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
get cache() {
|
|
476
|
+
return this._cryptoCache;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
get voltFingerprint() {
|
|
480
|
+
return this._voltFingerprint;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
getKey() {
|
|
484
|
+
const privateKey = pki$1.privateKeyFromPem(this._cryptoCache.key);
|
|
485
|
+
const publicKey = pki$1.rsa.setPublicKey(privateKey.n, privateKey.e);
|
|
486
|
+
return {privateKey, publicKey};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Retrieves and re-hydrates the pub/priv key data from the crypto cache, or creates one if not found.
|
|
491
|
+
*/
|
|
492
|
+
createKey() {
|
|
493
|
+
if (!this._cryptoCache.key) {
|
|
494
|
+
log$4("creating key");
|
|
495
|
+
return new Promise((resolve, reject) => {
|
|
496
|
+
pki$1.rsa.generateKeyPair({bits: 2048, workers: -1}, (err, keypair) => {
|
|
497
|
+
if (err) {
|
|
498
|
+
log$4("failure creating key [%s]", err.message);
|
|
499
|
+
reject(err);
|
|
500
|
+
} else {
|
|
501
|
+
log$4("successfully created key");
|
|
502
|
+
const pem = pki$1.privateKeyToPem(keypair.privateKey);
|
|
503
|
+
this._cryptoCache.key = pem;
|
|
504
|
+
resolve(keypair);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
// Already have key data in the cache in pem format => create fully-formed pki instances.
|
|
510
|
+
log$4("createKey - key already exists");
|
|
511
|
+
return Promise.resolve(this.getKey());
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Create cryptocache from scratch.
|
|
517
|
+
*/
|
|
518
|
+
initialise() {
|
|
519
|
+
return this.createKey()
|
|
520
|
+
.then((/* keypair */) => {
|
|
521
|
+
return this.saveCache();
|
|
522
|
+
})
|
|
523
|
+
.then(() => {
|
|
524
|
+
return this._cryptoCache;
|
|
525
|
+
})
|
|
526
|
+
.catch((err) => {
|
|
527
|
+
log$4("failure initialising crypto [%s]", err.message);
|
|
528
|
+
return Promise.reject(err);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
get isBound() {
|
|
533
|
+
return !!this._cryptoCache.cert;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Signs the identity resource id using the private key.
|
|
538
|
+
* @param {*} audience the token audience (usually the target volt)
|
|
539
|
+
* @param {*} tunnelling flag indicating if the token is required for a tunnelled connection
|
|
540
|
+
* @param {*} ttl time to live in seconds (default to 1 minute)
|
|
541
|
+
*/
|
|
542
|
+
getIdentityToken(audience, tunnelling = false, ttl = 60) {
|
|
543
|
+
if (!this._cryptoCache.key) {
|
|
544
|
+
throw new Error("crypto cache invalid");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const keyPair = this.getKey();
|
|
548
|
+
const publicKeyPem = pki$1.publicKeyToPem(keyPair.publicKey);
|
|
549
|
+
const base58Key = createPublicKeyFingerprint(publicKeyPem);
|
|
550
|
+
|
|
551
|
+
log$4("base58 key is %s", base58Key);
|
|
552
|
+
|
|
553
|
+
// Allow TTL either side of the current time.
|
|
554
|
+
// @todo - fix with server time sync on volt connection.
|
|
555
|
+
const payload = {
|
|
556
|
+
aud: audience,
|
|
557
|
+
iat: Math.floor(Date.now() / 1000) - ttl,
|
|
558
|
+
exp: Math.floor(Date.now() / 1000) + ttl,
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
let sharedKey;
|
|
562
|
+
if (tunnelling) {
|
|
563
|
+
// Initialise the tunnel encryption key.
|
|
564
|
+
sharedKey = VoltCrypto.aesCreateKey();
|
|
565
|
+
|
|
566
|
+
// Encrypt the key details using the target Volt public key and include this in the JWT payload.
|
|
567
|
+
payload.sk = VoltCrypto.rsaEncrypt(this._voltPublicKey, sharedKey.key);
|
|
568
|
+
payload.iv = VoltCrypto.rsaEncrypt(this._voltPublicKey, sharedKey.iv);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Sign synchronously.
|
|
572
|
+
const token = jwt__default["default"].sign(payload, this._cryptoCache.key, {algorithm: rs256Algorithm, keyid: base58Key});
|
|
573
|
+
|
|
574
|
+
// Return the token along with any encryption key details.
|
|
575
|
+
return {
|
|
576
|
+
token,
|
|
577
|
+
sharedKey,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Creates the metadata required for a grpc call, including the Volt authentication token
|
|
583
|
+
* and optionally tunnel encryption parameters.
|
|
584
|
+
* @param {*} grpc
|
|
585
|
+
* @param {*} audience
|
|
586
|
+
* @param {*} tunnelling
|
|
587
|
+
* @param {*} ttl
|
|
588
|
+
* @returns
|
|
589
|
+
*/
|
|
590
|
+
getIdentityMetadata(grpc, audience, tunnelling = false, ttl = 10) {
|
|
591
|
+
const identityToken = this.getIdentityToken(audience || this._voltConfig.id, tunnelling, ttl);
|
|
592
|
+
const metadata = new grpc.Metadata();
|
|
593
|
+
metadata.add(constants.authTokenName, identityToken.token);
|
|
594
|
+
return {...identityToken, metadata};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
saveCache() {
|
|
598
|
+
if (this._persistCache) {
|
|
599
|
+
const clone = {...this._config, crypto: {...this._cryptoCache}};
|
|
600
|
+
if (this._config.p) {
|
|
601
|
+
// A passphrase option exists => encrypt the key before writing the cache file.
|
|
602
|
+
const privateKey = pki$1.privateKeyFromPem(this._cryptoCache.key);
|
|
603
|
+
clone.crypto.key = pki$1.encryptRsaPrivateKey(privateKey, this._config.p);
|
|
604
|
+
}
|
|
605
|
+
fs__default["default"].writeFileSync(this._configPath, JSON.stringify(clone, null, 2));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
static aesCreateKey() {
|
|
610
|
+
// Generate random key for the AES-256 algorithm (32 bytes = 256 bits).
|
|
611
|
+
const key = crypto__default["default"].randomBytes(32);
|
|
612
|
+
const iv = crypto__default["default"].randomBytes(16);
|
|
613
|
+
return {key, iv};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
static aesEncrypt(buffer, key, iv) {
|
|
617
|
+
const cipher = crypto__default["default"].createCipheriv(aesAlgorithm, key, iv);
|
|
618
|
+
const crypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
619
|
+
return crypted;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
static aesDecrypt(buffer, key, iv) {
|
|
623
|
+
const cipher = crypto__default["default"].createDecipheriv(aesAlgorithm, key, iv);
|
|
624
|
+
const decrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
625
|
+
return decrypted;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
static rsaEncrypt(key, buffer) {
|
|
629
|
+
return crypto__default["default"].publicEncrypt(key, buffer).toString("base64");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const {get: getProp, snakeCase} = lodash__default["default"];
|
|
634
|
+
const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
635
|
+
|
|
636
|
+
const defaultProtoPath = path.join(__dirname$1, "../protobuf");
|
|
637
|
+
|
|
638
|
+
const defaultLoaderOptions = {
|
|
639
|
+
keepCase: true,
|
|
640
|
+
longs: String,
|
|
641
|
+
enums: String,
|
|
642
|
+
defaults: true,
|
|
643
|
+
oneofs: true,
|
|
644
|
+
includeDirs: [defaultProtoPath],
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const getProtoDescriptors = (grpc, protoPath, opts) => {
|
|
648
|
+
const definition = protoLoader__default["default"].loadSync(protoPath, opts || defaultLoaderOptions);
|
|
649
|
+
const descriptors = grpc.loadPackageDefinition(definition);
|
|
650
|
+
return descriptors;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const getProtoService = (descriptors, servicePath) => {
|
|
654
|
+
const serviceDef = getProp(descriptors, servicePath);
|
|
655
|
+
if (serviceDef && serviceDef.service) {
|
|
656
|
+
return serviceDef.service;
|
|
657
|
+
} else {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
function getServiceDescriptorsFromPath(grpc, protoPath, servicePackageName, opts) {
|
|
663
|
+
let serviceDescriptors;
|
|
664
|
+
const descriptors = getProtoDescriptors(grpc, protoPath, opts);
|
|
665
|
+
if (descriptors) {
|
|
666
|
+
serviceDescriptors = getProtoService(descriptors, servicePackageName);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return serviceDescriptors;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function getServiceDescriptors(grpc, servicePackageName, opts) {
|
|
673
|
+
// Extract the package components of the service proto package. The package name should be of the
|
|
674
|
+
// fully qualified proto path, e.g. tdx.volt_api.webcam.v1.WebcamControlAPI.
|
|
675
|
+
const protoServiceComponents = servicePackageName.split(".");
|
|
676
|
+
|
|
677
|
+
// The service name is the last component of the path.
|
|
678
|
+
const protoServiceName = protoServiceComponents.pop();
|
|
679
|
+
|
|
680
|
+
// The actual file name should match the snake case of the service name.
|
|
681
|
+
const protoFileName = snakeCase(protoServiceName).toLowerCase() + ".proto";
|
|
682
|
+
|
|
683
|
+
// The path to the proto file should match each component of the package name.
|
|
684
|
+
const protoPath = path.join(defaultProtoPath, ...protoServiceComponents, protoFileName);
|
|
685
|
+
|
|
686
|
+
let serviceDescriptors;
|
|
687
|
+
const descriptors = getProtoDescriptors(grpc, protoPath, opts);
|
|
688
|
+
if (descriptors) {
|
|
689
|
+
serviceDescriptors = getProtoService(descriptors, servicePackageName);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return serviceDescriptors;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function getProtoDescriptorMethods(protoDescriptor) {
|
|
696
|
+
const methods = [];
|
|
697
|
+
Object.keys(protoDescriptor).forEach((methodName) => {
|
|
698
|
+
const method = protoDescriptor[methodName];
|
|
699
|
+
methods.push({
|
|
700
|
+
path: method.path,
|
|
701
|
+
client_streaming: method.requestStream,
|
|
702
|
+
server_streaming: method.responseStream,
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
return methods;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
var protoUtils = /*#__PURE__*/Object.freeze({
|
|
709
|
+
__proto__: null,
|
|
710
|
+
getServiceDescriptors: getServiceDescriptors,
|
|
711
|
+
getProtoDescriptors: getProtoDescriptors,
|
|
712
|
+
getProtoDescriptorMethods: getProtoDescriptorMethods,
|
|
713
|
+
getServiceDescriptorsFromPath: getServiceDescriptorsFromPath
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
/* eslint-disable no-underscore-dangle */
|
|
717
|
+
|
|
718
|
+
const log$3 = debug__default["default"]("volt-client-grpc:grpc-call");
|
|
719
|
+
|
|
720
|
+
function _prepareInvokeRequest(request, method, methodType = "METHOD_TYPE_UNARY") {
|
|
721
|
+
const meta = this._voltClient.crypto.getIdentityMetadata(
|
|
722
|
+
this._voltClient.grpc,
|
|
723
|
+
this._voltClient.options.id,
|
|
724
|
+
this._voltClient.isRemote
|
|
725
|
+
);
|
|
726
|
+
let invokeRequest;
|
|
727
|
+
let callMethod = method;
|
|
728
|
+
|
|
729
|
+
if (this._voltClient.isVoltTunnel) {
|
|
730
|
+
let requestPayload;
|
|
731
|
+
if (request) {
|
|
732
|
+
requestPayload = this._grpcClient[method].requestSerialize(request);
|
|
733
|
+
} else if (request !== null) {
|
|
734
|
+
log$3("****************LOOKOUT****************** - empty request");
|
|
735
|
+
} else {
|
|
736
|
+
log$3("explicit empty request payload");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const tunnelMethod = this._grpcClient.Tunnel;
|
|
740
|
+
|
|
741
|
+
const tunnelResp = {
|
|
742
|
+
method_invoke: {
|
|
743
|
+
method_name: this._grpcClient[method].path,
|
|
744
|
+
method_type: methodType,
|
|
745
|
+
token: meta.token,
|
|
746
|
+
request: requestPayload,
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const invokePayload = VoltCrypto.aesEncrypt(
|
|
751
|
+
tunnelMethod.responseSerialize(tunnelResp),
|
|
752
|
+
meta.sharedKey.key,
|
|
753
|
+
meta.sharedKey.iv
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
log$3("target volt is %s", this._voltClient.crypto.voltFingerprint);
|
|
757
|
+
|
|
758
|
+
invokeRequest = {
|
|
759
|
+
identity_fingerprint: this._voltClient.crypto.voltFingerprint,
|
|
760
|
+
payload: invokePayload,
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
callMethod = "Invoke";
|
|
764
|
+
} else if (this._voltClient.isRemote && meta.sharedKey.key) {
|
|
765
|
+
const requestPayload = this._grpcClient[method].requestSerializeOriginal(request);
|
|
766
|
+
invokeRequest = VoltCrypto.aesEncrypt(requestPayload, meta.sharedKey.key, meta.sharedKey.iv);
|
|
767
|
+
} else {
|
|
768
|
+
invokeRequest = request;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
meta,
|
|
773
|
+
method: callMethod,
|
|
774
|
+
methodName: method,
|
|
775
|
+
request: invokeRequest,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function _preparePayloadRequest(request, invokeInfo) {
|
|
780
|
+
let payloadRequest;
|
|
781
|
+
|
|
782
|
+
if (this._voltClient.isVoltTunnel) {
|
|
783
|
+
const requestPayload = this._grpcClient[invokeInfo.methodName].requestSerialize(request);
|
|
784
|
+
const tunnelMethod = this._grpcClient.Tunnel;
|
|
785
|
+
|
|
786
|
+
const tunnelResp = {
|
|
787
|
+
method_payload: {
|
|
788
|
+
payload: requestPayload,
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const invokePayload = VoltCrypto.aesEncrypt(
|
|
793
|
+
tunnelMethod.responseSerialize(tunnelResp),
|
|
794
|
+
invokeInfo.meta.sharedKey.key,
|
|
795
|
+
invokeInfo.meta.sharedKey.iv
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
payloadRequest = {
|
|
799
|
+
payload: invokePayload,
|
|
800
|
+
};
|
|
801
|
+
} else if (this._voltClient.isRemote && invokeInfo.meta.sharedKey.key) {
|
|
802
|
+
const requestPayload = this._grpcClient[invokeInfo.methodName].requestSerializeOriginal(request);
|
|
803
|
+
payloadRequest = VoltCrypto.aesEncrypt(requestPayload, invokeInfo.meta.sharedKey.key, invokeInfo.meta.sharedKey.iv);
|
|
804
|
+
} else {
|
|
805
|
+
payloadRequest = request;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return payloadRequest;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function _parseResponse(method, meta, response) {
|
|
812
|
+
let invokeResponse = {};
|
|
813
|
+
|
|
814
|
+
if (this._voltClient.isVoltTunnel) {
|
|
815
|
+
if (response.payload) {
|
|
816
|
+
const decryptedPayload = VoltCrypto.aesDecrypt(response.payload, meta.sharedKey.key, meta.sharedKey.iv);
|
|
817
|
+
const tunnelMethod = this._grpcClient.Tunnel;
|
|
818
|
+
const responsePayload = tunnelMethod.requestDeserialize(decryptedPayload);
|
|
819
|
+
if (responsePayload.payload === "method_payload") {
|
|
820
|
+
invokeResponse.payload = this._grpcClient[method].responseDeserialize(responsePayload.method_payload.payload);
|
|
821
|
+
} else if (responsePayload.payload === "method_end") {
|
|
822
|
+
invokeResponse = responsePayload.method_end;
|
|
823
|
+
} else {
|
|
824
|
+
log$3("unexpected tunnel payload type: %s", responsePayload.payload);
|
|
825
|
+
}
|
|
826
|
+
} else if (response.status && response.status.message) {
|
|
827
|
+
// There's been an error in the tunnel.
|
|
828
|
+
log$3(
|
|
829
|
+
"Tunnel status received: %s, code %d, description: %s",
|
|
830
|
+
response.status.message,
|
|
831
|
+
response.status.code,
|
|
832
|
+
response.status.description || "n/a"
|
|
833
|
+
);
|
|
834
|
+
invokeResponse = {ended: true, error: `Error in tunnel: ${response.status.message}`};
|
|
835
|
+
} else {
|
|
836
|
+
invokeResponse.methodId = response.invoke_id;
|
|
837
|
+
log$3("started method %d for %s", invokeResponse.methodId, method);
|
|
838
|
+
}
|
|
839
|
+
} else if (this._voltClient.isRemote && meta.sharedKey.key) {
|
|
840
|
+
const decryptedPayload = VoltCrypto.aesDecrypt(response, meta.sharedKey.key, meta.sharedKey.iv);
|
|
841
|
+
invokeResponse = {payload: this._grpcClient[method].responseDeserializeOriginal(decryptedPayload)};
|
|
842
|
+
} else {
|
|
843
|
+
invokeResponse = {payload: response};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return invokeResponse;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
class GRPCCall extends EventEmitter__default["default"] {
|
|
850
|
+
constructor(voltClient, methodName, methodType) {
|
|
851
|
+
super();
|
|
852
|
+
this._methodName = methodName;
|
|
853
|
+
this._methodType = methodType;
|
|
854
|
+
this._call = null;
|
|
855
|
+
this._voltClient = voltClient;
|
|
856
|
+
this._lastResponse = null;
|
|
857
|
+
this._promise = null;
|
|
858
|
+
this._reject = null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
reject(err) {
|
|
862
|
+
if (this._reject) {
|
|
863
|
+
this._reject(err);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
wait() {
|
|
868
|
+
return this._promise;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
start(grpcClient, request, responseCB) {
|
|
872
|
+
this._promise = new Promise((resolve, reject) => {
|
|
873
|
+
this._grpcClient = grpcClient;
|
|
874
|
+
this._reject = reject;
|
|
875
|
+
|
|
876
|
+
const callbackIntercept = (err, response) => {
|
|
877
|
+
log$3("received callback for method %s", this._methodName);
|
|
878
|
+
if (err) {
|
|
879
|
+
reject(err);
|
|
880
|
+
} else if (response) {
|
|
881
|
+
this._lastResponse = response;
|
|
882
|
+
if (responseCB) {
|
|
883
|
+
responseCB(err, response);
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
this.end();
|
|
887
|
+
resolve(this._lastResponse);
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
this._initialRequest = _prepareInvokeRequest.call(this, request, this._methodName, this._methodType);
|
|
892
|
+
|
|
893
|
+
this._call = this._grpcClient[this._initialRequest.method](this._initialRequest.meta.metadata, callbackIntercept);
|
|
894
|
+
|
|
895
|
+
this._call.on("data", (streamResponse) => {
|
|
896
|
+
try {
|
|
897
|
+
const parsedResponse = _parseResponse.call(this, this._methodName, this._initialRequest.meta, streamResponse);
|
|
898
|
+
if (parsedResponse.payload) {
|
|
899
|
+
callbackIntercept(null, parsedResponse.payload);
|
|
900
|
+
} else if (parsedResponse.ended) {
|
|
901
|
+
if (parsedResponse.error) {
|
|
902
|
+
callbackIntercept(new Error(parsedResponse.error), null);
|
|
903
|
+
} else {
|
|
904
|
+
callbackIntercept(null, null);
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
log$3("method id is %s", parsedResponse.methodId);
|
|
908
|
+
}
|
|
909
|
+
} catch (err) {
|
|
910
|
+
log$3("failure processing connect response: %s", err.message);
|
|
911
|
+
this.emit("error", err);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
this._call.on("error", (err) => {
|
|
916
|
+
log$3("ERROR - intercepted stream error %s", err.message);
|
|
917
|
+
callbackIntercept(err, null);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
this._call.on("finish", () => {
|
|
921
|
+
// The read side has ended (i.e. we called end()).
|
|
922
|
+
log$3("finished call %s", this._methodName);
|
|
923
|
+
this.emit("finish");
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
this._call.on("end", () => {
|
|
927
|
+
// The remote peer ended the stream.
|
|
928
|
+
log$3("ended call %s, method id: %s", this._methodName, this._initialRequest.methodId || "n/a - not tunnelling");
|
|
929
|
+
this.emit("end");
|
|
930
|
+
callbackIntercept(null, null);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
if (this._initialRequest.request) {
|
|
934
|
+
this._call.write(this._initialRequest.request);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
return this;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
end() {
|
|
942
|
+
if (!this._call) {
|
|
943
|
+
throw new Error("call not initialised");
|
|
944
|
+
} else {
|
|
945
|
+
return this._call.end();
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
get writable() {
|
|
950
|
+
return this._call && this._call.writable;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
write(request) {
|
|
954
|
+
if (!this._call) {
|
|
955
|
+
log$3("ERROR - call not initialised");
|
|
956
|
+
throw new Error("call not initialised");
|
|
957
|
+
} else if (!this._initialRequest) {
|
|
958
|
+
this._initialRequest = _prepareInvokeRequest.call(this, request, this._methodName, this._methodType);
|
|
959
|
+
|
|
960
|
+
return this._call.write(this._initialRequest.request);
|
|
961
|
+
} else {
|
|
962
|
+
const payloadRequest = _preparePayloadRequest.call(this, request, this._initialRequest);
|
|
963
|
+
return this._call.write(payloadRequest);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/* eslint-disable no-use-before-define */
|
|
969
|
+
|
|
970
|
+
const log$2 = debug__default["default"]("volt-client-grpc:volt-connection");
|
|
971
|
+
|
|
972
|
+
function _cleanUp(immediateReconnect) {
|
|
973
|
+
this._session = false;
|
|
974
|
+
|
|
975
|
+
if (this._pingTimer) {
|
|
976
|
+
clearTimeout(this._pingTimer);
|
|
977
|
+
this._pingTimer = 0;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (this._pingTimeoutTimer) {
|
|
981
|
+
clearTimeout(this._pingTimeoutTimer);
|
|
982
|
+
this._pingTimeoutTimer = 0;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (!this._dying && (immediateReconnect || (this._autoRetry && !this._reconnectTimer))) {
|
|
986
|
+
this._reconnectTimer = setTimeout(
|
|
987
|
+
() => {
|
|
988
|
+
this._reconnectTimer = 0;
|
|
989
|
+
_doConnect.call(this);
|
|
990
|
+
},
|
|
991
|
+
immediateReconnect ? 0 : this._reconnectInterval
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (this.call && this.call.writable) {
|
|
996
|
+
this.call.end();
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function _connectionTimeoutHandler() {
|
|
1001
|
+
log$2("server timed out");
|
|
1002
|
+
this.emit("connected", false);
|
|
1003
|
+
_cleanUp.call(this);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function _doConnect() {
|
|
1007
|
+
this.call = new GRPCCall(this._voltClient, "Connect", "METHOD_TYPE_BIDI");
|
|
1008
|
+
|
|
1009
|
+
const grpcClient = this._voltClient.getVoltAPIClient();
|
|
1010
|
+
const helloPayload = {
|
|
1011
|
+
hello: {},
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
this.call.start(grpcClient, helloPayload, (err, response) => {
|
|
1015
|
+
if (err) {
|
|
1016
|
+
log$2("error on connection stream [%s]", err.message);
|
|
1017
|
+
_cleanUp.call(this);
|
|
1018
|
+
this.emit("error", err);
|
|
1019
|
+
} else {
|
|
1020
|
+
switch (response.payload) {
|
|
1021
|
+
case "ping": {
|
|
1022
|
+
log$2("pinged");
|
|
1023
|
+
|
|
1024
|
+
// Received ping response from server => clear timeout.
|
|
1025
|
+
if (this._pingTimeoutTimer) {
|
|
1026
|
+
clearTimeout(this._pingTimeoutTimer);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Schedule the next ping.
|
|
1030
|
+
_startPingTimer.call(this);
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
case "hello":
|
|
1034
|
+
if (!this._session) {
|
|
1035
|
+
this._session = true;
|
|
1036
|
+
this.emit("connected", this._session);
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
case "evt":
|
|
1040
|
+
log$2("received event %s", response.evt.event_type);
|
|
1041
|
+
this.emit("evt", response.evt);
|
|
1042
|
+
break;
|
|
1043
|
+
default:
|
|
1044
|
+
log$2("unimplemented connect response payload: %s", response.payload);
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
this.call
|
|
1051
|
+
.wait()
|
|
1052
|
+
.then(() => {
|
|
1053
|
+
if (this._session) {
|
|
1054
|
+
this.emit("connected", false);
|
|
1055
|
+
}
|
|
1056
|
+
})
|
|
1057
|
+
.catch((err) => {
|
|
1058
|
+
log$2("unexpected error on connect call [%s]", err.message);
|
|
1059
|
+
})
|
|
1060
|
+
.finally(() => {
|
|
1061
|
+
_cleanUp.call(this);
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
_startPingTimer.call(this);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function _startPingTimer() {
|
|
1068
|
+
if (this._pingTimer) {
|
|
1069
|
+
log$2("ping timer running");
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
this._pingTimer = setTimeout(() => {
|
|
1074
|
+
// Send next ping to server.
|
|
1075
|
+
const request = {ping: {timestamp: Date.now()}};
|
|
1076
|
+
|
|
1077
|
+
log$2("pinging Volt");
|
|
1078
|
+
this.call.write(request);
|
|
1079
|
+
|
|
1080
|
+
this._pingTimer = 0;
|
|
1081
|
+
|
|
1082
|
+
// Schedule a timeout if we don't hear back from the server.
|
|
1083
|
+
if (this._pingTimeoutTimer) {
|
|
1084
|
+
clearTimeout(this._pingTimeoutTimer);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
this._pingTimeoutTimer = setTimeout(_connectionTimeoutHandler.bind(this), this._timeoutInterval);
|
|
1088
|
+
}, this._pingInterval);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
class VoltConnection extends EventEmitter__default["default"] {
|
|
1092
|
+
constructor(voltClient, grpc, options = {}) {
|
|
1093
|
+
super();
|
|
1094
|
+
|
|
1095
|
+
this._grpc = grpc;
|
|
1096
|
+
this._autoRetry = options.autoReconnect === undefined ? true : !!options.autoReconnect;
|
|
1097
|
+
this._dying = false;
|
|
1098
|
+
this._voltClient = voltClient;
|
|
1099
|
+
this._options = options;
|
|
1100
|
+
this._pingTimer = 0;
|
|
1101
|
+
this._pingTimeoutTimer = 0;
|
|
1102
|
+
this._session = false;
|
|
1103
|
+
this._pingInterval = options.pingInterval || 10000;
|
|
1104
|
+
this._reconnectInterval = options.reconnectInterval || 5000;
|
|
1105
|
+
this._timeoutInterval = options.timeoutInterval || 60000;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
connect(throwOnFail) {
|
|
1109
|
+
if (throwOnFail) {
|
|
1110
|
+
return _doConnect.call(this);
|
|
1111
|
+
} else {
|
|
1112
|
+
return Promise.resolve(_cleanUp.call(this, true));
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
disconnect() {
|
|
1117
|
+
this._dying = true;
|
|
1118
|
+
_cleanUp.call(this);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/* eslint-disable no-underscore-dangle */
|
|
1123
|
+
|
|
1124
|
+
const {pki} = forge__default["default"];
|
|
1125
|
+
const {filter, pick} = lodash__default["default"];
|
|
1126
|
+
|
|
1127
|
+
const log$1 = debug__default["default"]("volt-client-grpc:volt-client-internal");
|
|
1128
|
+
const voltServices = [
|
|
1129
|
+
constants.serviceType.voltAPI,
|
|
1130
|
+
constants.serviceType.fileAPI,
|
|
1131
|
+
constants.serviceType.sqliteServerAPI,
|
|
1132
|
+
constants.serviceType.ssiAPI,
|
|
1133
|
+
constants.serviceType.tunnelAPI,
|
|
1134
|
+
constants.serviceType.wireAPI,
|
|
1135
|
+
];
|
|
1136
|
+
|
|
1137
|
+
function issueBind(bindRequest, ttl) {
|
|
1138
|
+
// eslint-disable-next-line no-use-before-define
|
|
1139
|
+
return unaryCall.call(this, "Bind", bindRequest).then((bindResponse) => {
|
|
1140
|
+
log$1("got binding request response %j", bindResponse);
|
|
1141
|
+
if (bindResponse.status && bindResponse.status.code) {
|
|
1142
|
+
log$1("error in bind response: %s", bindResponse.status.message);
|
|
1143
|
+
return Promise.reject(new Error(bindResponse.status.message));
|
|
1144
|
+
} else {
|
|
1145
|
+
switch (bindResponse.decision) {
|
|
1146
|
+
case "POLICY_DECISION_PERMIT":
|
|
1147
|
+
//
|
|
1148
|
+
// The request status is permit => cache the bind response info.
|
|
1149
|
+
//
|
|
1150
|
+
|
|
1151
|
+
// This is the certificate assigned to us by the volt.
|
|
1152
|
+
this._crypto.cache.cert = bindResponse.cert;
|
|
1153
|
+
|
|
1154
|
+
// This is the signing CA used by the volt (don't ov)
|
|
1155
|
+
this._crypto.cache.ca = bindResponse.chain;
|
|
1156
|
+
|
|
1157
|
+
// Resource id is assigned by the volt.
|
|
1158
|
+
this._crypto.cache.client_id = bindResponse.identity_id;
|
|
1159
|
+
this._crypto.saveCache();
|
|
1160
|
+
break;
|
|
1161
|
+
case "POLICY_DECISION_DENY":
|
|
1162
|
+
log$1(">>>>>>>>>>>>>>> access request is DENIED <<<<<<<<<<<<<<<<<<<");
|
|
1163
|
+
break;
|
|
1164
|
+
case "POLICY_DECISION_PROMPT":
|
|
1165
|
+
case "POLICY_DECISION_PENDING":
|
|
1166
|
+
log$1("access pending approval - waiting...");
|
|
1167
|
+
break;
|
|
1168
|
+
default:
|
|
1169
|
+
log$1("ignoring unknown bind descision %s", bindResponse.status);
|
|
1170
|
+
break;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
return bindResponse.decision;
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function findDIDDocumentService(document, serviceType) {
|
|
1179
|
+
if (!document.service) {
|
|
1180
|
+
return [];
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const matches = filter(document.service, (svc) => svc.type === serviceType);
|
|
1184
|
+
return matches;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
async function bindInternal() {
|
|
1188
|
+
try {
|
|
1189
|
+
// Build grpc client info.
|
|
1190
|
+
if (this.isRemote) {
|
|
1191
|
+
const getJSON = bent__default["default"]("json");
|
|
1192
|
+
// @todo - currently Volt tunnel CA discovery is not secure.
|
|
1193
|
+
const protocol =
|
|
1194
|
+
this._config.tunnel_host?.startsWith("localhost:") || this._config.cloud_host?.startsWith("localhost:")
|
|
1195
|
+
? "http"
|
|
1196
|
+
: "https";
|
|
1197
|
+
const tunnelURL = `${protocol}://${this._config.tunnel_host || this._config.cloud_host}/info`;
|
|
1198
|
+
log$1("attempting to retrieve tunnel information from %s", tunnelURL);
|
|
1199
|
+
const tunnelInfo = await getJSON(tunnelURL);
|
|
1200
|
+
if (!tunnelInfo.ca_pem) {
|
|
1201
|
+
throw new Error("Unable to fetch tunnel CA - cannot securely connect.");
|
|
1202
|
+
} else {
|
|
1203
|
+
log$1("Auto-fetched tunnel CA %s, remote address %s", tunnelInfo.ca_pem, tunnelInfo.local_address);
|
|
1204
|
+
this._crypto.cache.tunnel_ca_pem = tunnelInfo.ca_pem;
|
|
1205
|
+
this._crypto.cache.tunnel_address = tunnelInfo.local_address;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (!this._crypto.cache.ca) {
|
|
1210
|
+
this._crypto.cache.ca = this._options.ca_pem;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (!this._crypto.cache.ca) {
|
|
1214
|
+
throw new Error("No certificate authority found in configuration for target Volt - check configuration");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const keyPair = this._crypto.getKey();
|
|
1218
|
+
const publicKeyPem = pki.publicKeyToPem(keyPair.publicKey);
|
|
1219
|
+
|
|
1220
|
+
if (!this._crypto.cache.bindIp) {
|
|
1221
|
+
// Default the IP address if none is specified. This is used to set the SAN in the
|
|
1222
|
+
// certificate issued by the Volt.
|
|
1223
|
+
this._crypto.cache.bindIp = ip__default["default"].address();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
this._crypto.saveCache();
|
|
1227
|
+
|
|
1228
|
+
// Form the request message.
|
|
1229
|
+
const bindRequest = {
|
|
1230
|
+
binding_name: this._config.client_name,
|
|
1231
|
+
public_key: publicKeyPem,
|
|
1232
|
+
host: this._crypto.cache.bindIp,
|
|
1233
|
+
x509_credential: [],
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
if (this._crypto.cache.cert) {
|
|
1237
|
+
bindRequest.x509_credential = [this._crypto.cache.cert + this._crypto.cache.ca];
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (this._options.challenge_code) {
|
|
1241
|
+
bindRequest.challenge = signBase64(keyPair.privateKey, this._options.challenge_code);
|
|
1242
|
+
} else {
|
|
1243
|
+
log$1("****no Volt challenge code available**** => not sending challenge signature");
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// For remote connections, add our cloud-issued certificate as an additional credential.
|
|
1247
|
+
if (this._crypto.cache.tunnel_ca_pem && this._crypto.cache.cloud_cert) {
|
|
1248
|
+
bindRequest.x509_credential = bindRequest.x509_credential.concat(
|
|
1249
|
+
this._crypto.cache.cloud_cert + this._crypto.cache.tunnel_ca_pem
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
log$1("sending %d x509 credentials", bindRequest.x509_credential.length);
|
|
1254
|
+
log$1("bind TTL is ", this._config.bindRequestTTL);
|
|
1255
|
+
|
|
1256
|
+
// This will block while the request is pending.
|
|
1257
|
+
const pollInterval = 10000;
|
|
1258
|
+
let decision = "";
|
|
1259
|
+
do {
|
|
1260
|
+
if (decision) {
|
|
1261
|
+
// Wait a while before retrying.
|
|
1262
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1263
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1264
|
+
}
|
|
1265
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1266
|
+
decision = await issueBind.call(this, bindRequest, this._options.bindRequestTTL);
|
|
1267
|
+
log$1("binding decision: %s", decision);
|
|
1268
|
+
} while (decision === "POLICY_DECISION_PROMPT" || decision === "POLICY_DECISION_PENDING");
|
|
1269
|
+
|
|
1270
|
+
return decision === "POLICY_DECISION_PERMIT";
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
log$1("failure binding to Volt [%s]", err.message);
|
|
1273
|
+
throw err;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function connectInternal() {
|
|
1278
|
+
try {
|
|
1279
|
+
if (this._voltConnection) {
|
|
1280
|
+
log$1("connectInternal - Volt connection already exists");
|
|
1281
|
+
return Promise.resolve();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const onDisconnected = () => {
|
|
1285
|
+
this._connected = false;
|
|
1286
|
+
this._session = null;
|
|
1287
|
+
|
|
1288
|
+
if (this._cachedClient) {
|
|
1289
|
+
this._grpc.closeClient(this._cachedClient);
|
|
1290
|
+
this._cachedClient = null;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (this._cachedRemoteClient) {
|
|
1294
|
+
this._grpc.closeClient(this._cachedRemoteClient);
|
|
1295
|
+
this._cachedRemoteClient = null;
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
this._voltConnection = new VoltConnection(this, this._grpc, this._options);
|
|
1300
|
+
|
|
1301
|
+
this._voltConnection.on("connected", (session) => {
|
|
1302
|
+
this._session = session;
|
|
1303
|
+
this._connected = !!session;
|
|
1304
|
+
if (!this._connected) {
|
|
1305
|
+
onDisconnected();
|
|
1306
|
+
}
|
|
1307
|
+
this.emit("connected", !!session);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
this._voltConnection.on("error", (err) => {
|
|
1311
|
+
// Should auto-reconnect.
|
|
1312
|
+
log$1("Volt connection error [%s]", err.message);
|
|
1313
|
+
if (this._connected) {
|
|
1314
|
+
onDisconnected();
|
|
1315
|
+
this.emit("connected", false);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
this._voltConnection.on("evt", (evt) => {
|
|
1320
|
+
this.emit("evt", evt);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
return this._voltConnection.connect();
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
log$1("connect - error [%s]", err.message);
|
|
1326
|
+
throw err;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function getVoltAPIClientInternal() {
|
|
1331
|
+
if (!this._cachedClient) {
|
|
1332
|
+
const serviceAddress = this.isRemote ? this._crypto.cache.tunnel_address : this._options.local_address;
|
|
1333
|
+
log$1("creating cached VoltAPI service client on %s", serviceAddress);
|
|
1334
|
+
let serviceDescriptors = {};
|
|
1335
|
+
voltServices.forEach((pkg) => {
|
|
1336
|
+
const packageDescriptors = getServiceDescriptors(this._grpc, pkg);
|
|
1337
|
+
serviceDescriptors = {...serviceDescriptors, ...packageDescriptors};
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
this._cachedClient = createClient(
|
|
1341
|
+
this._grpc,
|
|
1342
|
+
serviceDescriptors,
|
|
1343
|
+
serviceAddress,
|
|
1344
|
+
this._crypto.cache,
|
|
1345
|
+
this.isRemote && !this.isVoltTunnel,
|
|
1346
|
+
this._config.cloud_host ? this._options.id : "",
|
|
1347
|
+
this._config.cloud_host ? this._options.id : ""
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
return this._cachedClient;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function unaryCall(method, request) {
|
|
1355
|
+
const grpcClient = this.getVoltAPIClient();
|
|
1356
|
+
|
|
1357
|
+
const call = new GRPCCall(this, method, "METHOD_TYPE_UNARY");
|
|
1358
|
+
call.start(grpcClient, request);
|
|
1359
|
+
return call.wait();
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
async function fetchVoltConfig(volt_did) {
|
|
1363
|
+
const didResolution = getDIDResolutionURL(volt_did);
|
|
1364
|
+
if (!didResolution) {
|
|
1365
|
+
throw new Error(`Invalid Volt DID: ${volt_did}`);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const getJSON = bent__default["default"]("json");
|
|
1369
|
+
const didDocument = await getJSON(didResolution);
|
|
1370
|
+
const voltConfigServices = findDIDDocumentService(didDocument, constants.didServiceType.voltConfig);
|
|
1371
|
+
if (voltConfigServices.length !== 1) {
|
|
1372
|
+
log$1(
|
|
1373
|
+
"Unexpected number of Volt configuration endpoints found in DID document for %s, services found: %d",
|
|
1374
|
+
volt_did,
|
|
1375
|
+
voltConfigServices.length
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
const serviceEndpoint = voltConfigServices[0].serviceEndpoint;
|
|
1379
|
+
const resolvedConfig = await getJSON(serviceEndpoint);
|
|
1380
|
+
|
|
1381
|
+
const voltConfig = pick(
|
|
1382
|
+
resolvedConfig,
|
|
1383
|
+
"id",
|
|
1384
|
+
"display_name",
|
|
1385
|
+
"public_key",
|
|
1386
|
+
"fingerprint",
|
|
1387
|
+
"local_address",
|
|
1388
|
+
"ca_pem",
|
|
1389
|
+
"challenge_code"
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
return {
|
|
1393
|
+
volt: voltConfig,
|
|
1394
|
+
tunnel_ca_pem: resolvedConfig.tunnel_ca_pem,
|
|
1395
|
+
tunnel_address: resolvedConfig.tunnel_address,
|
|
1396
|
+
cloud_host: resolvedConfig.tunnel_address.split(":")[0],
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const {readFile} = fs__default["default"].promises;
|
|
1401
|
+
const log = debug__default["default"]("volt-client-grpc:volt-client");
|
|
1402
|
+
|
|
1403
|
+
class VoltClient extends EventEmitter__default["default"] {
|
|
1404
|
+
/**
|
|
1405
|
+
* @param {object} grpc - an instance of the grpc module (see https://www.npmjs.com/package/grpc)
|
|
1406
|
+
*/
|
|
1407
|
+
constructor(grpc) {
|
|
1408
|
+
super();
|
|
1409
|
+
|
|
1410
|
+
if (!grpc) {
|
|
1411
|
+
throw new Error("invalid arguments, grpc is required");
|
|
1412
|
+
}
|
|
1413
|
+
this._grpc = grpc;
|
|
1414
|
+
this._cachedClient = null;
|
|
1415
|
+
this._cachedRemoteClient = null;
|
|
1416
|
+
this._session = null;
|
|
1417
|
+
this._config = null;
|
|
1418
|
+
this._options = null;
|
|
1419
|
+
this._voltConnection = null;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
get config() {
|
|
1423
|
+
return this._config;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
get crypto() {
|
|
1427
|
+
return this._crypto;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
get grpc() {
|
|
1431
|
+
return this._grpc;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
get options() {
|
|
1435
|
+
return this._options;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Initialises a binding and connection to the volt.
|
|
1440
|
+
* @param {string} [config] - the location of the Volt configuration file, or a configuration object
|
|
1441
|
+
*/
|
|
1442
|
+
async initialise(config, extras = {}) {
|
|
1443
|
+
try {
|
|
1444
|
+
if (!config) {
|
|
1445
|
+
throw new Error("configPath argument is required to be a non-empty string");
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
let configPath;
|
|
1449
|
+
let configJSON;
|
|
1450
|
+
if (typeof config === "string") {
|
|
1451
|
+
configPath = path__default["default"].resolve(config);
|
|
1452
|
+
log("Attempt to load config from file %s", configPath);
|
|
1453
|
+
try {
|
|
1454
|
+
const configContents = await readFile(new URL(configPath, (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
1455
|
+
configJSON = JSON.parse(configContents);
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
log("failure loading config file %s [%s]", configPath, err.message);
|
|
1458
|
+
throw new Error(`failed to load configuration - check JSON format in ${configPath}`);
|
|
1459
|
+
}
|
|
1460
|
+
} else if (typeof config === "object") {
|
|
1461
|
+
configJSON = config;
|
|
1462
|
+
} else {
|
|
1463
|
+
throw new Error("config must be either a file path or a config object");
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (!configJSON || typeof configJSON !== "object") {
|
|
1467
|
+
throw new Error("config argument is required to be an object");
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (!configJSON.client_name || typeof configJSON.client_name !== "string") {
|
|
1471
|
+
throw new Error("client_name property required in config");
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
this._config = {...configJSON, ...extras};
|
|
1475
|
+
|
|
1476
|
+
if (this._config.volt_did && !this._config.volt) {
|
|
1477
|
+
// No Volt configuration, attempt to discover it via the given DID.
|
|
1478
|
+
const didConfig = await fetchVoltConfig.call(this, this._config.volt_did);
|
|
1479
|
+
log("resolved config from DID: %s", JSON.stringify(didConfig, null, 2));
|
|
1480
|
+
this._config = {...this._config, ...didConfig};
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
this._options = this._config.volt;
|
|
1484
|
+
|
|
1485
|
+
if (!this._options || typeof this._options !== "object") {
|
|
1486
|
+
throw new Error("configuration is missing the 'volt' object");
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (!this._options.id || typeof this._options.id !== "string") {
|
|
1490
|
+
throw new Error("'id' property is missing in Volt configuration");
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
this._crypto = new VoltCrypto(this._config, configPath);
|
|
1494
|
+
|
|
1495
|
+
await this._crypto.initialise();
|
|
1496
|
+
|
|
1497
|
+
if (!this.isRemote) {
|
|
1498
|
+
// If we're not connecting via a tunnel, we must have the address and CA.
|
|
1499
|
+
if (!this._options.local_address || typeof this._options.local_address !== "string") {
|
|
1500
|
+
throw new Error("'local_address' property missing in Volt configuration");
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (!this._options.ca_pem || typeof this._options.ca_pem !== "string") {
|
|
1504
|
+
throw new Error("'ca_pPem' property missing in Volt configuration");
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
let isBound;
|
|
1509
|
+
if (!this._crypto._cryptoCache.cert) {
|
|
1510
|
+
// No certificate present in the cache yet => we need to bind.
|
|
1511
|
+
try {
|
|
1512
|
+
isBound = await bindInternal.call(this);
|
|
1513
|
+
} catch (bindErr) {
|
|
1514
|
+
if (bindErr.message === "invalid arguments") {
|
|
1515
|
+
// This is usually the result of a missing challenge signature or credential.
|
|
1516
|
+
log("failure binding to Volt - did you supply a challenge code or verified credential?");
|
|
1517
|
+
throw bindErr;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
} else {
|
|
1521
|
+
isBound = true;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (!isBound) {
|
|
1525
|
+
// This is usually because of request being denied.
|
|
1526
|
+
throw new Error("Volt binding failed");
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
return Promise.resolve();
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
log("Failure starting Volt client [%s]", err.message);
|
|
1532
|
+
throw err;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
initialiseAndConnect(configPath, extras = {}) {
|
|
1537
|
+
return this.initialise(configPath, extras)
|
|
1538
|
+
.then(() => {
|
|
1539
|
+
return connectInternal.call(this);
|
|
1540
|
+
})
|
|
1541
|
+
.catch((err) => {
|
|
1542
|
+
log("startAndConnect failure: %s", err.message);
|
|
1543
|
+
return Promise.reject(err);
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Creates a GRPC client for the given Volt service description.
|
|
1549
|
+
*
|
|
1550
|
+
* Automatically handles remote connections.
|
|
1551
|
+
*
|
|
1552
|
+
* n.b. for this javascript client Protobuf definitions should conform to the recommended
|
|
1553
|
+
* structure, which is that a package proto file resides in a folder path that matches the
|
|
1554
|
+
* package name, and services contained in the package are named in in Pascal Case and using
|
|
1555
|
+
* the suffix 'API'. For example the WebcamControlAPI in package tdx.volt_api.webcam.v1 should reside
|
|
1556
|
+
* in a protobuf file at tdx/api/webcam/v1/webcam_control_api.proto
|
|
1557
|
+
*
|
|
1558
|
+
* @param {object} service a service description.
|
|
1559
|
+
* @param {object} descriptors the service descriptors, must be specified unless the service
|
|
1560
|
+
* packages are well-known Volt APIs.
|
|
1561
|
+
*/
|
|
1562
|
+
createServiceClient(service, descriptors) {
|
|
1563
|
+
const pkg = service.service_description.service_api;
|
|
1564
|
+
|
|
1565
|
+
let serviceDescriptors;
|
|
1566
|
+
if (!descriptors) {
|
|
1567
|
+
serviceDescriptors = {};
|
|
1568
|
+
pkg.forEach((svc) => {
|
|
1569
|
+
log("adding service %s to service client", svc);
|
|
1570
|
+
const packageDescriptors = getServiceDescriptors(this._grpc, svc);
|
|
1571
|
+
serviceDescriptors = {...serviceDescriptors, ...packageDescriptors};
|
|
1572
|
+
});
|
|
1573
|
+
} else {
|
|
1574
|
+
serviceDescriptors = descriptors;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
if (this.isRemote) {
|
|
1578
|
+
if (!this._crypto.cache.tunnel_address) {
|
|
1579
|
+
throw new Error("Remote connection enabled but no tunnel address - have you called start()?");
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// In remote mode => create a client using the discovered tunnel address **and** passing the service reourceId
|
|
1583
|
+
return createClient(
|
|
1584
|
+
this._grpc,
|
|
1585
|
+
serviceDescriptors,
|
|
1586
|
+
this._crypto.cache.tunnel_address,
|
|
1587
|
+
this._crypto.cache,
|
|
1588
|
+
false,
|
|
1589
|
+
service.volt_id,
|
|
1590
|
+
service.id
|
|
1591
|
+
);
|
|
1592
|
+
} else {
|
|
1593
|
+
return createClient(
|
|
1594
|
+
this._grpc,
|
|
1595
|
+
serviceDescriptors,
|
|
1596
|
+
service.service_description.local_address,
|
|
1597
|
+
this._crypto.cache
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
get isRemote() {
|
|
1603
|
+
return (this._config.cloud_host || this._config.tunnel_host) && this._options.id;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
get isVoltTunnel() {
|
|
1607
|
+
return this.isRemote && this._config.tunnel_host;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
connect() {
|
|
1611
|
+
return connectInternal.call(this);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
disconnect() {
|
|
1615
|
+
if (this._voltConnection) {
|
|
1616
|
+
this._voltConnection.disconnect();
|
|
1617
|
+
this._voltConnection = null;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
getVoltAPIClient() {
|
|
1622
|
+
return getVoltAPIClientInternal.call(this);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Request resource access.
|
|
1627
|
+
* @param {*} targetResourceId
|
|
1628
|
+
* @param {*} accessType - see vault.proto for enum values.
|
|
1629
|
+
*/
|
|
1630
|
+
async RequestAccessBlocking(targetResourceId, accessType = "VOLT_ACCESS_READ") {
|
|
1631
|
+
try {
|
|
1632
|
+
const accessRequest = {
|
|
1633
|
+
access: accessType,
|
|
1634
|
+
resource_id: targetResourceId,
|
|
1635
|
+
};
|
|
1636
|
+
|
|
1637
|
+
// This will block while the request is pending.
|
|
1638
|
+
// TODO - implement streamed rpc on volt? Not sure there is a need really...
|
|
1639
|
+
const pollInterval = 10000;
|
|
1640
|
+
let decision = "";
|
|
1641
|
+
do {
|
|
1642
|
+
if (decision) {
|
|
1643
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1644
|
+
}
|
|
1645
|
+
const accessResponse = await this.RequestAccess(accessRequest);
|
|
1646
|
+
decision = accessResponse.decision;
|
|
1647
|
+
log("access decision: %s", decision);
|
|
1648
|
+
} while (decision === "POLICY_DECISION_PROMPT" || decision === "POLICY_DECISION_PENDING");
|
|
1649
|
+
|
|
1650
|
+
log("requestResourceAccess result is %j", decision);
|
|
1651
|
+
return decision === "POLICY_DECISION_PERMIT";
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
log("failure requesting resource access [%s]", err.message);
|
|
1654
|
+
return Promise.reject(err);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* VoltAPI - resource management
|
|
1660
|
+
*/
|
|
1661
|
+
|
|
1662
|
+
CanAccessResource(request) {
|
|
1663
|
+
return unaryCall.call(this, "CanAccessResource", request);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
DeleteResource(request) {
|
|
1667
|
+
return unaryCall.call(this, "DeleteResource", request);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
DiscoverServices(request) {
|
|
1671
|
+
return unaryCall.call(this, "DiscoverServices", request);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
GetResource(request) {
|
|
1675
|
+
return unaryCall.call(this, "GetResource", request);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
GetResources(request) {
|
|
1679
|
+
return unaryCall.call(this, "GetResources", request);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
GetResourceAncestors(request) {
|
|
1683
|
+
return unaryCall.call(this, "GetResourceAncestors", request);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
GetResourceDescendants(request) {
|
|
1687
|
+
return unaryCall.call(this, "GetResourceDescendants", request);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
RequestAccess(request) {
|
|
1691
|
+
return unaryCall.call(this, "RequestAccess", request);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
SaveResource(request) {
|
|
1695
|
+
return unaryCall.call(this, "SaveResource", request);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
SaveResourceAttribute(request) {
|
|
1699
|
+
return unaryCall.call(this, "SaveResourceAttribute", request);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
SetServiceStatus(request) {
|
|
1703
|
+
return unaryCall.call(this, "SetServiceStatus", request);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* VoltAPI - volt management
|
|
1708
|
+
*/
|
|
1709
|
+
|
|
1710
|
+
Bind(request) {
|
|
1711
|
+
return unaryCall.call(this, "Bind", request);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
DeleteAccess(request) {
|
|
1715
|
+
return unaryCall.call(this, "DeleteAccess", request);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
DeleteVolt(request) {
|
|
1719
|
+
return unaryCall.call(this, "DeleteVolt", request);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
GetAccess(request) {
|
|
1723
|
+
return unaryCall.call(this, "GetAccess", request);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
GetBindings(request) {
|
|
1727
|
+
return unaryCall.call(this, "GetBindings", request);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
GetIdentities(request) {
|
|
1731
|
+
return unaryCall.call(this, "GetIdentities", request);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
GetIdentity(request) {
|
|
1735
|
+
return unaryCall.call(this, "GetIdentity", request);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
GetIdentityToken(request) {
|
|
1739
|
+
return unaryCall.call(this, "GetIdentityToken", request);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
GetPolicy(request) {
|
|
1743
|
+
return unaryCall.call(this, "GetPolicy", request);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
GetSettings(request) {
|
|
1747
|
+
return unaryCall.call(this, "GetSettings", request);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
SaveAccess(request) {
|
|
1751
|
+
return unaryCall.call(this, "SaveAccess", request);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
SaveCloudConnection(request) {
|
|
1755
|
+
return unaryCall.call(this, "SaveCloudConnection", request);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
SaveIdentity(request) {
|
|
1759
|
+
return unaryCall.call(this, "SaveIdentity", request);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
SaveSettings(request) {
|
|
1763
|
+
return unaryCall.call(this, "SaveSettings", request);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
SetAccessRequestDecision(request) {
|
|
1767
|
+
return unaryCall.call(this, "SetAccessRequestDecision", request);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
SetBindingDecision(request) {
|
|
1771
|
+
return unaryCall.call(this, "SetBindingDecision", request);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
Shutdown(request) {
|
|
1775
|
+
return unaryCall.call(this, "Shutdown", request);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
SignVerify(request) {
|
|
1779
|
+
return unaryCall.call(this, "SignVerify", request);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* FileAPI
|
|
1784
|
+
*/
|
|
1785
|
+
GetFileDescendants(request) {
|
|
1786
|
+
return unaryCall.call(this, "GetFileDescendants", request);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/**
|
|
1790
|
+
* Asynchronous file download
|
|
1791
|
+
* @param {*} request
|
|
1792
|
+
* @returns stream
|
|
1793
|
+
*/
|
|
1794
|
+
DownloadFile(request) {
|
|
1795
|
+
const grpcClient = this.getVoltAPIClient();
|
|
1796
|
+
|
|
1797
|
+
return new Promise((resolve, reject) => {
|
|
1798
|
+
const meta = this._crypto.getIdentityMetadata(this._grpc, this._options.id, this.isRemote);
|
|
1799
|
+
resolve(grpcClient.DownloadFile(request, meta.metadata));
|
|
1800
|
+
}).catch((err) => {
|
|
1801
|
+
log("error during %s [%s]", method, err.message);
|
|
1802
|
+
return Promise.reject(err);
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
UploadFile(request, callback) {
|
|
1807
|
+
const grpcClient = this.getVoltAPIClient();
|
|
1808
|
+
|
|
1809
|
+
const uploadCall = new GRPCCall(this, "UploadFile", "METHOD_TYPE_BIDI");
|
|
1810
|
+
return uploadCall.start(grpcClient, request, callback);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Synchronous file download
|
|
1815
|
+
* @param {*} request
|
|
1816
|
+
* @returns file buffer, base64 encoded
|
|
1817
|
+
*/
|
|
1818
|
+
DownloadFileSync(request) {
|
|
1819
|
+
const grpcClient = this.getVoltAPIClient();
|
|
1820
|
+
|
|
1821
|
+
const blocks = [];
|
|
1822
|
+
|
|
1823
|
+
const call = new GRPCCall(this, "DownloadFile", "METHOD_TYPE_SERVER_STREAM");
|
|
1824
|
+
call.start(grpcClient, request, (err, response) => {
|
|
1825
|
+
if (err) {
|
|
1826
|
+
return call.reject(err);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
switch (response.payload) {
|
|
1830
|
+
case "block":
|
|
1831
|
+
blocks.push(response.block);
|
|
1832
|
+
break;
|
|
1833
|
+
case "status":
|
|
1834
|
+
if (response.status.message) {
|
|
1835
|
+
return call.reject(new Error(response.status.message));
|
|
1836
|
+
}
|
|
1837
|
+
break;
|
|
1838
|
+
default:
|
|
1839
|
+
log("unrecognised response payload %s", response.payload);
|
|
1840
|
+
break;
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1844
|
+
return call
|
|
1845
|
+
.wait()
|
|
1846
|
+
.then(() => {
|
|
1847
|
+
log("Download finished");
|
|
1848
|
+
return {buffer: Buffer.concat(blocks).toString("base64")};
|
|
1849
|
+
})
|
|
1850
|
+
.catch((err) => {
|
|
1851
|
+
log("error during DownloadFile [%s]", err.message);
|
|
1852
|
+
return Promise.reject(err);
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* WireAPI
|
|
1858
|
+
*/
|
|
1859
|
+
PublishWire(request, callback) {
|
|
1860
|
+
const grpcClient = this.getVoltAPIClient();
|
|
1861
|
+
|
|
1862
|
+
const publishCall = new GRPCCall(this, "PublishWire", "METHOD_TYPE_CLIENT_STREAM");
|
|
1863
|
+
return publishCall.start(grpcClient, request, callback);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
SubscribeWire(request, callback) {
|
|
1867
|
+
const grpcClient = this.getVoltAPIClient();
|
|
1868
|
+
|
|
1869
|
+
const subscribeCall = new GRPCCall(this, "SubscribeWire", "METHOD_TYPE_BIDI");
|
|
1870
|
+
return subscribeCall.start(grpcClient, request, callback);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
Object.defineProperty(exports, 'generateId', {
|
|
1875
|
+
enumerable: true,
|
|
1876
|
+
get: function () { return uuid.v4; }
|
|
1877
|
+
});
|
|
1878
|
+
exports.VoltClient = VoltClient;
|
|
1879
|
+
exports.VoltCrypto = VoltCrypto;
|
|
1880
|
+
exports.constants = constants;
|
|
1881
|
+
exports.grpcUtils = grpcUtils;
|
|
1882
|
+
exports.protoUtils = protoUtils;
|
|
1883
|
+
exports.utils = utils;
|