@tdxvolt/volt-client-grpc 0.14.61 → 0.15.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/lib/index.cjs +785 -224
- package/package.json +1 -1
- package/protobuf/tdx/volt_api/volt/v1/file_api.proto +1 -1
- package/protobuf/tdx/volt_api/volt/v1/volt_api.proto +2 -0
- package/protobuf/tdx/volt_api/volt/v1/wire_api.proto +1 -1
- package/src/grpc-call.js +110 -49
- package/src/grpc-utils.js +9 -9
- package/src/proto-utils.js +37 -5
- package/src/rpc-invocation.js +277 -0
- package/src/utils.js +43 -2
- package/src/volt-client-internal.js +155 -58
- package/src/volt-client.js +95 -46
- package/src/volt-connection.js +16 -5
- package/src/volt-credential.js +32 -26
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import "tdx/volt_api/volt/v1/file.proto";
|
|
|
8
8
|
// The File API exposes basic file management functions.
|
|
9
9
|
service FileAPI {
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Download from file resource.
|
|
12
12
|
rpc DownloadFile(DownloadFileRequest) returns (stream DownloadFileResponse);
|
|
13
13
|
|
|
14
14
|
// Get file resource metadata.
|
|
@@ -787,8 +787,10 @@ message InvokeRequest {
|
|
|
787
787
|
// Indicates the client has ended the invocation.
|
|
788
788
|
bool client_end = 6;
|
|
789
789
|
|
|
790
|
+
// Reserved for internal use.
|
|
790
791
|
uint32 hop_index = 7;
|
|
791
792
|
|
|
793
|
+
// Reserved for internal use.
|
|
792
794
|
string target_service_id = 8;
|
|
793
795
|
}
|
|
794
796
|
|
|
@@ -6,7 +6,7 @@ import "tdx/volt_api/volt/v1/status.proto";
|
|
|
6
6
|
|
|
7
7
|
// The Wire API allows clients to subscribe and publish to Volt wire resources.
|
|
8
8
|
service WireAPI {
|
|
9
|
-
// Establishes a
|
|
9
|
+
// Establishes a client-streaming call to the wire resource.
|
|
10
10
|
rpc PublishWire(stream PublishWireRequest) returns (stream PublishWireResponse);
|
|
11
11
|
|
|
12
12
|
// Establishes a bi-directional streaming call to the wire resource.
|
package/src/grpc-call.js
CHANGED
|
@@ -10,16 +10,31 @@ function _prepareInvokeRequest(
|
|
|
10
10
|
method,
|
|
11
11
|
methodType = "METHOD_TYPE_UNARY",
|
|
12
12
|
) {
|
|
13
|
-
const
|
|
13
|
+
const isServiceRelayed =
|
|
14
|
+
this._service?.service_description.host_type ===
|
|
15
|
+
"SERVICE_HOST_TYPE_RELAYED";
|
|
16
|
+
|
|
17
|
+
// Use the service's host client id and public key if the service is relayed.
|
|
18
|
+
const methodToken = this._voltClient.credential.getIdentityMetadata(
|
|
14
19
|
this._voltClient.grpc,
|
|
15
|
-
this.
|
|
16
|
-
|
|
20
|
+
this._service?.service_description.host_client_id ||
|
|
21
|
+
this._voltClient.voltConfig.id,
|
|
22
|
+
this._service?.service_description.host_public_key ||
|
|
23
|
+
this._voltClient.credential.voltPublicKey,
|
|
24
|
+
true,
|
|
17
25
|
);
|
|
26
|
+
|
|
18
27
|
let invokeRequest;
|
|
19
28
|
let callMethod = method;
|
|
20
29
|
|
|
21
|
-
if (this._voltClient.isVoltRelay) {
|
|
30
|
+
if (this._voltClient.isVoltRelay || isServiceRelayed) {
|
|
31
|
+
//
|
|
32
|
+
// If the volt connection is via a relay (or the target service is relayed),
|
|
33
|
+
// we wrap the rpc in a RemoteRequest message and send it via a call to Invoke().
|
|
34
|
+
//
|
|
22
35
|
let requestPayload;
|
|
36
|
+
|
|
37
|
+
// Serialise the rpc request.
|
|
23
38
|
if (request) {
|
|
24
39
|
requestPayload = this._grpcClient[method].requestSerialize(request);
|
|
25
40
|
} else if (request !== null) {
|
|
@@ -28,45 +43,61 @@ function _prepareInvokeRequest(
|
|
|
28
43
|
log("explicit empty request payload");
|
|
29
44
|
}
|
|
30
45
|
|
|
31
|
-
|
|
46
|
+
// Use this placeholder to serialise the relay request wrapper.
|
|
47
|
+
const voltGrpcClient = this._voltClient.getVoltAPIClient();
|
|
48
|
+
const tunnelMethod = voltGrpcClient.Tunnel;
|
|
32
49
|
|
|
33
50
|
const tunnelResp = {
|
|
34
51
|
method_invoke: {
|
|
35
52
|
method_name: this._grpcClient[method].path,
|
|
36
53
|
method_type: methodType,
|
|
37
|
-
token:
|
|
54
|
+
token: methodToken.token,
|
|
38
55
|
request: requestPayload,
|
|
39
56
|
},
|
|
40
57
|
};
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
let targetFingerprint = [];
|
|
60
|
+
let invokePayload;
|
|
61
|
+
if (this._voltClient.isVoltRelay || isServiceRelayed) {
|
|
62
|
+
// When sending via a relay we need to encrypt the payload.
|
|
63
|
+
invokePayload = VoltCredential.aesEncrypt(
|
|
64
|
+
tunnelMethod.responseSerialize(tunnelResp),
|
|
65
|
+
methodToken.sharedKey.key,
|
|
66
|
+
methodToken.sharedKey.iv,
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
invokePayload = tunnelMethod.responseSerialize(tunnelResp);
|
|
70
|
+
}
|
|
47
71
|
|
|
48
|
-
|
|
72
|
+
if (this._voltClient.isVoltRelay) {
|
|
73
|
+
targetFingerprint.push(this._voltClient.credential.voltFingerprint);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isServiceRelayed) {
|
|
77
|
+
// Add another relay hop if the target service is relayed.
|
|
78
|
+
log("target service is relayed");
|
|
79
|
+
targetFingerprint.push(
|
|
80
|
+
this._service.service_description.host_fingerprint,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log("target is %j", targetFingerprint);
|
|
49
85
|
|
|
50
86
|
invokeRequest = {
|
|
51
|
-
|
|
87
|
+
target_fingerprint: targetFingerprint,
|
|
88
|
+
token: methodToken.token,
|
|
52
89
|
payload: invokePayload,
|
|
90
|
+
target_service_id: isServiceRelayed ? this._service.id : undefined,
|
|
53
91
|
};
|
|
54
92
|
|
|
93
|
+
// Replace the target method with a call to Invoke on the relay Volt.
|
|
55
94
|
callMethod = "Invoke";
|
|
56
|
-
} else if (this._voltClient.isRemote && meta.sharedKey.key) {
|
|
57
|
-
const requestPayload =
|
|
58
|
-
this._grpcClient[method].requestSerializeOriginal(request);
|
|
59
|
-
invokeRequest = VoltCredential.aesEncrypt(
|
|
60
|
-
requestPayload,
|
|
61
|
-
meta.sharedKey.key,
|
|
62
|
-
meta.sharedKey.iv,
|
|
63
|
-
);
|
|
64
95
|
} else {
|
|
65
96
|
invokeRequest = request;
|
|
66
97
|
}
|
|
67
98
|
|
|
68
99
|
return {
|
|
69
|
-
meta,
|
|
100
|
+
meta: methodToken,
|
|
70
101
|
method: callMethod,
|
|
71
102
|
methodName: method,
|
|
72
103
|
request: invokeRequest,
|
|
@@ -76,10 +107,16 @@ function _prepareInvokeRequest(
|
|
|
76
107
|
function _preparePayloadRequest(request, invokeInfo) {
|
|
77
108
|
let payloadRequest;
|
|
78
109
|
|
|
79
|
-
|
|
110
|
+
const isServiceRelayed =
|
|
111
|
+
this._service?.service_description.host_type ===
|
|
112
|
+
"SERVICE_HOST_TYPE_RELAYED";
|
|
113
|
+
|
|
114
|
+
if (this._voltClient.isVoltRelay || isServiceRelayed) {
|
|
80
115
|
const requestPayload =
|
|
81
116
|
this._grpcClient[invokeInfo.methodName].requestSerialize(request);
|
|
82
|
-
|
|
117
|
+
|
|
118
|
+
const voltGrpcClient = this._voltClient.getVoltAPIClient();
|
|
119
|
+
const tunnelMethod = voltGrpcClient.Tunnel;
|
|
83
120
|
|
|
84
121
|
const tunnelResp = {
|
|
85
122
|
method_payload: {
|
|
@@ -96,14 +133,6 @@ function _preparePayloadRequest(request, invokeInfo) {
|
|
|
96
133
|
payloadRequest = {
|
|
97
134
|
payload: invokePayload,
|
|
98
135
|
};
|
|
99
|
-
} else if (this._voltClient.isRemote && invokeInfo.meta.sharedKey.key) {
|
|
100
|
-
const requestPayload =
|
|
101
|
-
this._grpcClient[invokeInfo.methodName].requestSerializeOriginal(request);
|
|
102
|
-
payloadRequest = VoltCredential.aesEncrypt(
|
|
103
|
-
requestPayload,
|
|
104
|
-
invokeInfo.meta.sharedKey.key,
|
|
105
|
-
invokeInfo.meta.sharedKey.iv,
|
|
106
|
-
);
|
|
107
136
|
} else {
|
|
108
137
|
payloadRequest = request;
|
|
109
138
|
}
|
|
@@ -112,21 +141,39 @@ function _preparePayloadRequest(request, invokeInfo) {
|
|
|
112
141
|
}
|
|
113
142
|
|
|
114
143
|
function _parseResponse(method, meta, response) {
|
|
144
|
+
// log("parseResponse for %s", this._methodName);
|
|
115
145
|
let invokeResponse = {};
|
|
116
146
|
|
|
117
|
-
|
|
147
|
+
const isServiceRelayed =
|
|
148
|
+
this._service?.service_description.host_type ===
|
|
149
|
+
"SERVICE_HOST_TYPE_RELAYED";
|
|
150
|
+
|
|
151
|
+
if (this._voltClient.isVoltRelay || isServiceRelayed) {
|
|
118
152
|
if (response.payload) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
153
|
+
let decryptedPayload;
|
|
154
|
+
if (meta.sharedKey.key) {
|
|
155
|
+
decryptedPayload = VoltCredential.aesDecrypt(
|
|
156
|
+
response.payload,
|
|
157
|
+
meta.sharedKey.key,
|
|
158
|
+
meta.sharedKey.iv,
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
decryptedPayload = response.payload;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const tunnelMethod = this._voltClient.getVoltAPIClient().Tunnel;
|
|
125
165
|
const responsePayload = tunnelMethod.requestDeserialize(decryptedPayload);
|
|
126
166
|
if (responsePayload.payload === "method_payload") {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
try {
|
|
168
|
+
invokeResponse.payload = this._grpcClient[method].responseDeserialize(
|
|
169
|
+
responsePayload.method_payload.payload,
|
|
170
|
+
);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
log("failure deserialising payload for %s", this._methodName);
|
|
173
|
+
throw new Error(
|
|
174
|
+
"failure deserialising payload - check protobuf definition matches with data being sent",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
130
177
|
} else if (responsePayload.payload === "method_end") {
|
|
131
178
|
invokeResponse = responsePayload.method_end;
|
|
132
179
|
} else {
|
|
@@ -166,17 +213,22 @@ function _parseResponse(method, meta, response) {
|
|
|
166
213
|
}
|
|
167
214
|
|
|
168
215
|
export default class GRPCCall extends EventEmitter {
|
|
169
|
-
constructor(voltClient, methodName, methodType) {
|
|
216
|
+
constructor(voltClient, methodName, methodType, service = undefined) {
|
|
170
217
|
super();
|
|
171
218
|
this._methodName = methodName;
|
|
172
219
|
this._methodType = methodType;
|
|
173
220
|
this._call = null;
|
|
174
221
|
this._voltClient = voltClient;
|
|
222
|
+
this._service = service;
|
|
175
223
|
}
|
|
176
224
|
|
|
177
225
|
start(grpcClient, request) {
|
|
178
226
|
this._grpcClient = grpcClient;
|
|
179
227
|
|
|
228
|
+
if (typeof this._grpcClient[this._methodName] !== "function") {
|
|
229
|
+
throw new Error(`method not found: '${this._methodName}'`);
|
|
230
|
+
}
|
|
231
|
+
|
|
180
232
|
this._initialRequest = _prepareInvokeRequest.call(
|
|
181
233
|
this,
|
|
182
234
|
request,
|
|
@@ -184,13 +236,18 @@ export default class GRPCCall extends EventEmitter {
|
|
|
184
236
|
this._methodType,
|
|
185
237
|
);
|
|
186
238
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
239
|
+
const isServiceRelayed =
|
|
240
|
+
this._service?.service_description.host_type ===
|
|
241
|
+
"SERVICE_HOST_TYPE_RELAYED";
|
|
242
|
+
|
|
243
|
+
let callClient;
|
|
244
|
+
if (this._voltClient.isVoltRelay || isServiceRelayed) {
|
|
245
|
+
callClient = this._voltClient.getVoltAPIClient();
|
|
246
|
+
} else {
|
|
247
|
+
callClient = this._grpcClient;
|
|
191
248
|
}
|
|
192
249
|
|
|
193
|
-
this._call =
|
|
250
|
+
this._call = callClient[this._initialRequest.method](
|
|
194
251
|
this._initialRequest.meta.metadata,
|
|
195
252
|
);
|
|
196
253
|
|
|
@@ -223,7 +280,11 @@ export default class GRPCCall extends EventEmitter {
|
|
|
223
280
|
log("method id is %s", parsedResponse.methodId);
|
|
224
281
|
}
|
|
225
282
|
} catch (err) {
|
|
226
|
-
log(
|
|
283
|
+
log(
|
|
284
|
+
"failure processing %s call response: %s",
|
|
285
|
+
this._methodName,
|
|
286
|
+
err.message,
|
|
287
|
+
);
|
|
227
288
|
this.emit("error", err);
|
|
228
289
|
}
|
|
229
290
|
});
|
package/src/grpc-utils.js
CHANGED
|
@@ -16,7 +16,7 @@ export function createClient(
|
|
|
16
16
|
credentials,
|
|
17
17
|
noSerialise,
|
|
18
18
|
voltId,
|
|
19
|
-
serviceId
|
|
19
|
+
serviceId
|
|
20
20
|
) {
|
|
21
21
|
let grpcCredentials;
|
|
22
22
|
if (credentials) {
|
|
@@ -30,7 +30,7 @@ export function createClient(
|
|
|
30
30
|
grpcCredentials = grpc.credentials.createSsl(
|
|
31
31
|
tlsOptions.ca,
|
|
32
32
|
tlsOptions.key,
|
|
33
|
-
tlsOptions.cert
|
|
33
|
+
tlsOptions.cert
|
|
34
34
|
);
|
|
35
35
|
} else {
|
|
36
36
|
// Cache has no private key, this implies we are connecting without supplying a client certificate.
|
|
@@ -92,7 +92,7 @@ const grpcWrap = (api) => {
|
|
|
92
92
|
// Support class instances and PoJos
|
|
93
93
|
const isClass = api.constructor !== Object;
|
|
94
94
|
const iterate = Object.getOwnPropertyNames(
|
|
95
|
-
isClass ? api.constructor.prototype : api
|
|
95
|
+
isClass ? api.constructor.prototype : api
|
|
96
96
|
);
|
|
97
97
|
|
|
98
98
|
// Wrap each method on the api.
|
|
@@ -106,7 +106,7 @@ const grpcWrap = (api) => {
|
|
|
106
106
|
// Enforce promise results.
|
|
107
107
|
if (!(resultPromise?.then && resultPromise?.catch)) {
|
|
108
108
|
throw new Error(
|
|
109
|
-
`implementation methods must return a Promise [${name}]
|
|
109
|
+
`implementation methods must return a Promise [${name}]`
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -143,7 +143,7 @@ class GrpcServer {
|
|
|
143
143
|
"grpc.http2.max_pings_without_data": 0,
|
|
144
144
|
"grpc.keepalive_permit_without_calls": 1,
|
|
145
145
|
"grpc.http2.min_ping_interval_without_data_ms": 250,
|
|
146
|
-
"grpc.http2.max_ping_strikes": 0
|
|
146
|
+
"grpc.http2.max_ping_strikes": 0
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
if (tlsOptions) {
|
|
@@ -153,7 +153,7 @@ class GrpcServer {
|
|
|
153
153
|
caList.push(tlsOption.ca);
|
|
154
154
|
certChains.push({
|
|
155
155
|
private_key: tlsOption.key,
|
|
156
|
-
cert_chain: tlsOption.cert
|
|
156
|
+
cert_chain: tlsOption.cert
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
159
|
|
|
@@ -164,7 +164,7 @@ class GrpcServer {
|
|
|
164
164
|
// The certificate chain(s) we present to the client.
|
|
165
165
|
certChains,
|
|
166
166
|
// Flag indicating if we insist on a client certificate.
|
|
167
|
-
requireClientCert
|
|
167
|
+
requireClientCert
|
|
168
168
|
);
|
|
169
169
|
} else {
|
|
170
170
|
this._credentials = grpc.ServerCredentials.createInsecure();
|
|
@@ -216,7 +216,7 @@ export function createServer(
|
|
|
216
216
|
serviceDescriptors,
|
|
217
217
|
routes,
|
|
218
218
|
cryptoCache,
|
|
219
|
-
requireClientCert
|
|
219
|
+
requireClientCert
|
|
220
220
|
) {
|
|
221
221
|
let tlsOptions;
|
|
222
222
|
if (cryptoCache) {
|
|
@@ -227,7 +227,7 @@ export function createServer(
|
|
|
227
227
|
serviceDescriptors,
|
|
228
228
|
routes,
|
|
229
229
|
tlsOptions,
|
|
230
|
-
requireClientCert
|
|
230
|
+
requireClientCert
|
|
231
231
|
};
|
|
232
232
|
return new GrpcServer(serverArgs);
|
|
233
233
|
}
|
package/src/proto-utils.js
CHANGED
|
@@ -2,6 +2,7 @@ import protoLoader from "@grpc/proto-loader";
|
|
|
2
2
|
import lodash from "lodash";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
5
6
|
|
|
6
7
|
const { get: getProp, snakeCase } = lodash;
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -17,23 +18,23 @@ const defaultLoaderOptions = {
|
|
|
17
18
|
includeDirs: [defaultProtoPath],
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
function getProtoDescriptors(grpc, protoPath, opts) {
|
|
21
22
|
const definition = protoLoader.loadSync(
|
|
22
23
|
protoPath,
|
|
23
24
|
opts || defaultLoaderOptions,
|
|
24
25
|
);
|
|
25
26
|
const descriptors = grpc.loadPackageDefinition(definition);
|
|
26
27
|
return descriptors;
|
|
27
|
-
}
|
|
28
|
+
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
function getProtoService(descriptors, servicePath) {
|
|
30
31
|
const serviceDef = getProp(descriptors, servicePath);
|
|
31
32
|
if (serviceDef?.service) {
|
|
32
33
|
return serviceDef.service;
|
|
33
34
|
} else {
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
|
-
}
|
|
37
|
+
}
|
|
37
38
|
|
|
38
39
|
function getServiceDescriptorsFromPath(
|
|
39
40
|
grpc,
|
|
@@ -41,8 +42,30 @@ function getServiceDescriptorsFromPath(
|
|
|
41
42
|
servicePackageName,
|
|
42
43
|
opts,
|
|
43
44
|
) {
|
|
45
|
+
if (!opts) {
|
|
46
|
+
opts = { ...defaultLoaderOptions };
|
|
47
|
+
opts.includeDirs = [protoPath];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extract the package components of the service proto package. The package name should be of the
|
|
51
|
+
// fully qualified proto path, e.g. tdx.volt_api.webcam.v1.WebcamControlAPI.
|
|
52
|
+
const protoServiceComponents = servicePackageName.split(".");
|
|
53
|
+
|
|
54
|
+
// The service name is the last component of the path.
|
|
55
|
+
const protoServiceName = protoServiceComponents.pop();
|
|
56
|
+
|
|
57
|
+
// The actual file name should match the snake case of the service name.
|
|
58
|
+
const protoFileName = `${snakeCase(protoServiceName).toLowerCase()}.proto`;
|
|
59
|
+
|
|
60
|
+
// The path to the proto file should match each component of the package name.
|
|
61
|
+
const serviceProtoPath = join(
|
|
62
|
+
protoPath,
|
|
63
|
+
...protoServiceComponents,
|
|
64
|
+
protoFileName,
|
|
65
|
+
);
|
|
66
|
+
|
|
44
67
|
let serviceDescriptors;
|
|
45
|
-
const descriptors = getProtoDescriptors(grpc,
|
|
68
|
+
const descriptors = getProtoDescriptors(grpc, serviceProtoPath, opts);
|
|
46
69
|
if (descriptors) {
|
|
47
70
|
serviceDescriptors = getProtoService(descriptors, servicePackageName);
|
|
48
71
|
}
|
|
@@ -90,7 +113,16 @@ function getProtoDescriptorMethods(protoDescriptor) {
|
|
|
90
113
|
return methods;
|
|
91
114
|
}
|
|
92
115
|
|
|
116
|
+
function createServiceProtobufFiles(service, protoPath) {
|
|
117
|
+
for (let protoFile of service.service_description.proto_file) {
|
|
118
|
+
const filePath = join(protoPath, protoFile.file_path);
|
|
119
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
120
|
+
writeFileSync(filePath, protoFile.protobuf);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
93
124
|
export {
|
|
125
|
+
createServiceProtobufFiles,
|
|
94
126
|
getServiceDescriptors,
|
|
95
127
|
getProtoDescriptors,
|
|
96
128
|
getProtoDescriptorMethods,
|