@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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package tdx.volt_api.volt.v1;
|
|
4
|
+
|
|
5
|
+
import "tdx/volt_api/volt/v1/status.proto";
|
|
6
|
+
|
|
7
|
+
// The Wire API allows clients to subscribe and publish to Volt wire resources.
|
|
8
|
+
service WireAPI {
|
|
9
|
+
// Establishes a client-streaming call to the wire resource.
|
|
10
|
+
rpc PublishWire(stream PublishRequest) returns (PublishResponse);
|
|
11
|
+
|
|
12
|
+
// Establishes a bi-directional streaming call to the wire resource.
|
|
13
|
+
// Although we're only really interested in receiving data from the wire, a bi-directional stream is required so that we can gracefully stop the subscription.
|
|
14
|
+
rpc SubscribeWire(stream SubscribeRequest) returns (stream SubscribeResponse);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
message PublishRequest {
|
|
18
|
+
// Only necessary in the first payload.
|
|
19
|
+
string wire_id = 1;
|
|
20
|
+
|
|
21
|
+
// The chunk of data to publish.
|
|
22
|
+
bytes chunk = 2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
message PublishResponse {
|
|
26
|
+
// Details of any error that occurred on the call.
|
|
27
|
+
tdx.volt_api.volt.v1.Status status = 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The request must include one of the following fields.
|
|
31
|
+
message SubscribeRequest {
|
|
32
|
+
oneof payload {
|
|
33
|
+
// The wire id, only required for the first message.
|
|
34
|
+
string wire_id = 1;
|
|
35
|
+
// Request to stop the subscription.
|
|
36
|
+
bool stop = 2;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// One of the following fields will be present in the response.
|
|
41
|
+
message SubscribeResponse {
|
|
42
|
+
oneof payload {
|
|
43
|
+
// Details of any error that occured on the call.
|
|
44
|
+
tdx.volt_api.volt.v1.Status status = 1;
|
|
45
|
+
|
|
46
|
+
// Data received from the wire.
|
|
47
|
+
bytes chunk = 2;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// eslint-disable-next-line import/prefer-default-export
|
|
2
|
+
export const constants = {
|
|
3
|
+
authTokenName: "tdx-token",
|
|
4
|
+
collectionOperation: {
|
|
5
|
+
add: "ADD",
|
|
6
|
+
remove: "REMOVE",
|
|
7
|
+
update: "UPDATE",
|
|
8
|
+
},
|
|
9
|
+
crypto: {
|
|
10
|
+
subjectDefaults: [
|
|
11
|
+
{
|
|
12
|
+
name: "countryName",
|
|
13
|
+
value: "GB",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "localityName",
|
|
17
|
+
value: "Southampton",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "organizationName",
|
|
21
|
+
value: "nquiringminds Ltd",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
userIdOID: "0.9.2342.19200300.100.1.1",
|
|
25
|
+
},
|
|
26
|
+
defaultDIDHostName: "volt.tdxcloud.com",
|
|
27
|
+
didServiceType: {
|
|
28
|
+
voltConfig: "tdx-volt-config",
|
|
29
|
+
},
|
|
30
|
+
identityType: {
|
|
31
|
+
did: "did",
|
|
32
|
+
volt: "volt",
|
|
33
|
+
},
|
|
34
|
+
onlineStatus: {
|
|
35
|
+
offline: "offline",
|
|
36
|
+
online: "online",
|
|
37
|
+
unknown: "unknown",
|
|
38
|
+
},
|
|
39
|
+
publicKeyPrefix: "-----BEGIN PUBLIC KEY-----",
|
|
40
|
+
privateKeyPrefix: "-----BEGIN RSA PRIVATE KEY-----",
|
|
41
|
+
privateEncryptedKeyPrefix: "-----BEGIN ENCRYPTED PRIVATE KEY-----",
|
|
42
|
+
resource: {
|
|
43
|
+
identity: "ident",
|
|
44
|
+
identityView: "ident-view",
|
|
45
|
+
proxy: "proxy",
|
|
46
|
+
proxyView: "proxy-view",
|
|
47
|
+
vc: "vc",
|
|
48
|
+
vcView: "vc-view",
|
|
49
|
+
},
|
|
50
|
+
voltStatus: {
|
|
51
|
+
pending: "",
|
|
52
|
+
online: "online",
|
|
53
|
+
offline: "offline",
|
|
54
|
+
deleted: "deleted",
|
|
55
|
+
revoked: "revoked",
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* These top-level service types should map to a protobuf definition
|
|
59
|
+
* in @tdxvolt/volt-proto.
|
|
60
|
+
*/
|
|
61
|
+
serviceType: {
|
|
62
|
+
fileAPI: "tdx.volt_api.volt.v1.FileAPI",
|
|
63
|
+
protocolBridgeDataStream: "tdx.volt_api.protocol_bridge.v1.ProtocolBridgeStreamAPI",
|
|
64
|
+
sqliteServerAPI: "tdx.volt_api.data.v1.SqliteServerAPI",
|
|
65
|
+
ssiAPI: "tdx.volt_api.ssi.v1.SsiAPI",
|
|
66
|
+
tunnelAPI: "tdx.volt_api.tunnel.v1.TunnelAPI",
|
|
67
|
+
voltAPI: "tdx.volt_api.volt.v1.VoltAPI",
|
|
68
|
+
wireAPI: "tdx.volt_api.volt.v1.WireAPI",
|
|
69
|
+
},
|
|
70
|
+
tunnelInvokeMethod: "/tdx.volt_api.volt.v1.VoltAPI/Invoke",
|
|
71
|
+
tunnelPingInterval: 10000,
|
|
72
|
+
tunnelPingTimeout: 2500,
|
|
73
|
+
tunnelReconnectAfter: 30000,
|
|
74
|
+
tunnelTimeoutInterval: 10000 * 2, // This must always be greater than `tunnelPingInterval`.
|
|
75
|
+
};
|
package/src/grpc-call.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
import debug from "debug";
|
|
3
|
+
import EventEmitter from "events";
|
|
4
|
+
import {VoltCrypto} from "./volt-crypto.js";
|
|
5
|
+
|
|
6
|
+
const log = debug("volt-client-grpc:grpc-call");
|
|
7
|
+
|
|
8
|
+
function _prepareInvokeRequest(request, method, methodType = "METHOD_TYPE_UNARY") {
|
|
9
|
+
const meta = this._voltClient.crypto.getIdentityMetadata(
|
|
10
|
+
this._voltClient.grpc,
|
|
11
|
+
this._voltClient.options.id,
|
|
12
|
+
this._voltClient.isRemote
|
|
13
|
+
);
|
|
14
|
+
let invokeRequest;
|
|
15
|
+
let callMethod = method;
|
|
16
|
+
|
|
17
|
+
if (this._voltClient.isVoltTunnel) {
|
|
18
|
+
let requestPayload;
|
|
19
|
+
if (request) {
|
|
20
|
+
requestPayload = this._grpcClient[method].requestSerialize(request);
|
|
21
|
+
} else if (request !== null) {
|
|
22
|
+
log("****************LOOKOUT****************** - empty request");
|
|
23
|
+
} else {
|
|
24
|
+
log("explicit empty request payload");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tunnelMethod = this._grpcClient.Tunnel;
|
|
28
|
+
|
|
29
|
+
const tunnelResp = {
|
|
30
|
+
method_invoke: {
|
|
31
|
+
method_name: this._grpcClient[method].path,
|
|
32
|
+
method_type: methodType,
|
|
33
|
+
token: meta.token,
|
|
34
|
+
request: requestPayload,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const invokePayload = VoltCrypto.aesEncrypt(
|
|
39
|
+
tunnelMethod.responseSerialize(tunnelResp),
|
|
40
|
+
meta.sharedKey.key,
|
|
41
|
+
meta.sharedKey.iv
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
log("target volt is %s", this._voltClient.crypto.voltFingerprint);
|
|
45
|
+
|
|
46
|
+
invokeRequest = {
|
|
47
|
+
identity_fingerprint: this._voltClient.crypto.voltFingerprint,
|
|
48
|
+
payload: invokePayload,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
callMethod = "Invoke";
|
|
52
|
+
} else if (this._voltClient.isRemote && meta.sharedKey.key) {
|
|
53
|
+
const requestPayload = this._grpcClient[method].requestSerializeOriginal(request);
|
|
54
|
+
invokeRequest = VoltCrypto.aesEncrypt(requestPayload, meta.sharedKey.key, meta.sharedKey.iv);
|
|
55
|
+
} else {
|
|
56
|
+
invokeRequest = request;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
meta,
|
|
61
|
+
method: callMethod,
|
|
62
|
+
methodName: method,
|
|
63
|
+
request: invokeRequest,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _preparePayloadRequest(request, invokeInfo) {
|
|
68
|
+
let payloadRequest;
|
|
69
|
+
|
|
70
|
+
if (this._voltClient.isVoltTunnel) {
|
|
71
|
+
const requestPayload = this._grpcClient[invokeInfo.methodName].requestSerialize(request);
|
|
72
|
+
const tunnelMethod = this._grpcClient.Tunnel;
|
|
73
|
+
|
|
74
|
+
const tunnelResp = {
|
|
75
|
+
method_payload: {
|
|
76
|
+
payload: requestPayload,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const invokePayload = VoltCrypto.aesEncrypt(
|
|
81
|
+
tunnelMethod.responseSerialize(tunnelResp),
|
|
82
|
+
invokeInfo.meta.sharedKey.key,
|
|
83
|
+
invokeInfo.meta.sharedKey.iv
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
payloadRequest = {
|
|
87
|
+
payload: invokePayload,
|
|
88
|
+
};
|
|
89
|
+
} else if (this._voltClient.isRemote && invokeInfo.meta.sharedKey.key) {
|
|
90
|
+
const requestPayload = this._grpcClient[invokeInfo.methodName].requestSerializeOriginal(request);
|
|
91
|
+
payloadRequest = VoltCrypto.aesEncrypt(requestPayload, invokeInfo.meta.sharedKey.key, invokeInfo.meta.sharedKey.iv);
|
|
92
|
+
} else {
|
|
93
|
+
payloadRequest = request;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return payloadRequest;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _parseResponse(method, meta, response) {
|
|
100
|
+
let invokeResponse = {};
|
|
101
|
+
|
|
102
|
+
if (this._voltClient.isVoltTunnel) {
|
|
103
|
+
if (response.payload) {
|
|
104
|
+
const decryptedPayload = VoltCrypto.aesDecrypt(response.payload, meta.sharedKey.key, meta.sharedKey.iv);
|
|
105
|
+
const tunnelMethod = this._grpcClient.Tunnel;
|
|
106
|
+
const responsePayload = tunnelMethod.requestDeserialize(decryptedPayload);
|
|
107
|
+
if (responsePayload.payload === "method_payload") {
|
|
108
|
+
invokeResponse.payload = this._grpcClient[method].responseDeserialize(responsePayload.method_payload.payload);
|
|
109
|
+
} else if (responsePayload.payload === "method_end") {
|
|
110
|
+
invokeResponse = responsePayload.method_end;
|
|
111
|
+
} else {
|
|
112
|
+
log("unexpected tunnel payload type: %s", responsePayload.payload);
|
|
113
|
+
}
|
|
114
|
+
} else if (response.status && response.status.message) {
|
|
115
|
+
// There's been an error in the tunnel.
|
|
116
|
+
log(
|
|
117
|
+
"Tunnel status received: %s, code %d, description: %s",
|
|
118
|
+
response.status.message,
|
|
119
|
+
response.status.code,
|
|
120
|
+
response.status.description || "n/a"
|
|
121
|
+
);
|
|
122
|
+
invokeResponse = {ended: true, error: `Error in tunnel: ${response.status.message}`};
|
|
123
|
+
} else {
|
|
124
|
+
invokeResponse.methodId = response.invoke_id;
|
|
125
|
+
log("started method %d for %s", invokeResponse.methodId, method);
|
|
126
|
+
}
|
|
127
|
+
} else if (this._voltClient.isRemote && meta.sharedKey.key) {
|
|
128
|
+
const decryptedPayload = VoltCrypto.aesDecrypt(response, meta.sharedKey.key, meta.sharedKey.iv);
|
|
129
|
+
invokeResponse = {payload: this._grpcClient[method].responseDeserializeOriginal(decryptedPayload)};
|
|
130
|
+
} else {
|
|
131
|
+
invokeResponse = {payload: response};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return invokeResponse;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default class GRPCCall extends EventEmitter {
|
|
138
|
+
constructor(voltClient, methodName, methodType) {
|
|
139
|
+
super();
|
|
140
|
+
this._methodName = methodName;
|
|
141
|
+
this._methodType = methodType;
|
|
142
|
+
this._call = null;
|
|
143
|
+
this._voltClient = voltClient;
|
|
144
|
+
this._lastResponse = null;
|
|
145
|
+
this._promise = null;
|
|
146
|
+
this._reject = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
reject(err) {
|
|
150
|
+
if (this._reject) {
|
|
151
|
+
this._reject(err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
wait() {
|
|
156
|
+
return this._promise;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
start(grpcClient, request, responseCB) {
|
|
160
|
+
this._promise = new Promise((resolve, reject) => {
|
|
161
|
+
this._grpcClient = grpcClient;
|
|
162
|
+
this._reject = reject;
|
|
163
|
+
|
|
164
|
+
const callbackIntercept = (err, response) => {
|
|
165
|
+
log("received callback for method %s", this._methodName);
|
|
166
|
+
if (err) {
|
|
167
|
+
reject(err);
|
|
168
|
+
} else if (response) {
|
|
169
|
+
this._lastResponse = response;
|
|
170
|
+
if (responseCB) {
|
|
171
|
+
responseCB(err, response);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
this.end();
|
|
175
|
+
resolve(this._lastResponse);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
this._initialRequest = _prepareInvokeRequest.call(this, request, this._methodName, this._methodType);
|
|
180
|
+
|
|
181
|
+
this._call = this._grpcClient[this._initialRequest.method](this._initialRequest.meta.metadata, callbackIntercept);
|
|
182
|
+
|
|
183
|
+
this._call.on("data", (streamResponse) => {
|
|
184
|
+
try {
|
|
185
|
+
const parsedResponse = _parseResponse.call(this, this._methodName, this._initialRequest.meta, streamResponse);
|
|
186
|
+
if (parsedResponse.payload) {
|
|
187
|
+
callbackIntercept(null, parsedResponse.payload);
|
|
188
|
+
} else if (parsedResponse.ended) {
|
|
189
|
+
if (parsedResponse.error) {
|
|
190
|
+
callbackIntercept(new Error(parsedResponse.error), null);
|
|
191
|
+
} else {
|
|
192
|
+
callbackIntercept(null, null);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
log("method id is %s", parsedResponse.methodId);
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
log("failure processing connect response: %s", err.message);
|
|
199
|
+
this.emit("error", err);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
this._call.on("error", (err) => {
|
|
204
|
+
log("ERROR - intercepted stream error %s", err.message);
|
|
205
|
+
callbackIntercept(err, null);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this._call.on("finish", () => {
|
|
209
|
+
// The read side has ended (i.e. we called end()).
|
|
210
|
+
log("finished call %s", this._methodName);
|
|
211
|
+
this.emit("finish");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this._call.on("end", () => {
|
|
215
|
+
// The remote peer ended the stream.
|
|
216
|
+
log("ended call %s, method id: %s", this._methodName, this._initialRequest.methodId || "n/a - not tunnelling");
|
|
217
|
+
this.emit("end");
|
|
218
|
+
callbackIntercept(null, null);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (this._initialRequest.request) {
|
|
222
|
+
this._call.write(this._initialRequest.request);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
end() {
|
|
230
|
+
if (!this._call) {
|
|
231
|
+
throw new Error("call not initialised");
|
|
232
|
+
} else {
|
|
233
|
+
return this._call.end();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
get writable() {
|
|
238
|
+
return this._call && this._call.writable;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
write(request) {
|
|
242
|
+
if (!this._call) {
|
|
243
|
+
log("ERROR - call not initialised");
|
|
244
|
+
throw new Error("call not initialised");
|
|
245
|
+
} else if (!this._initialRequest) {
|
|
246
|
+
this._initialRequest = _prepareInvokeRequest.call(this, request, this._methodName, this._methodType);
|
|
247
|
+
|
|
248
|
+
return this._call.write(this._initialRequest.request);
|
|
249
|
+
} else {
|
|
250
|
+
const payloadRequest = _preparePayloadRequest.call(this, request, this._initialRequest);
|
|
251
|
+
return this._call.write(payloadRequest);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
/* eslint-disable no-param-reassign */
|
|
3
|
+
import lodash from "lodash";
|
|
4
|
+
import {createSecureContextOptions} from "./utils.js";
|
|
5
|
+
|
|
6
|
+
const {forEach, isEmpty} = lodash;
|
|
7
|
+
|
|
8
|
+
const serialiseNOP = (arg) => {
|
|
9
|
+
return arg;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function createClient(grpc, serviceDescriptors, address, cryptoOptions, noSerialise, voltId, serviceId) {
|
|
13
|
+
let credentials;
|
|
14
|
+
if (cryptoOptions) {
|
|
15
|
+
const ca = cryptoOptions.tunnel_ca_pem || cryptoOptions.ca;
|
|
16
|
+
const cert = cryptoOptions.cloud_cert || cryptoOptions.cert;
|
|
17
|
+
const {key} = cryptoOptions;
|
|
18
|
+
|
|
19
|
+
if (key && cert) {
|
|
20
|
+
const cryyptoOptions = {ca, cert, key};
|
|
21
|
+
const tlsOptions = createSecureContextOptions(cryyptoOptions);
|
|
22
|
+
credentials = grpc.credentials.createSsl(tlsOptions.ca, tlsOptions.key, tlsOptions.cert);
|
|
23
|
+
} else {
|
|
24
|
+
// Cache has no private key, this implies we are connecting without supplying a client certificate.
|
|
25
|
+
// Simply connect using the CA certificate.
|
|
26
|
+
credentials = grpc.credentials.createSsl(Buffer.from(ca));
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
credentials = grpc.credentials.createInsecure();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Force all methods to appear bi-directional, since we manually handle the call flow (see grpc-call.js).
|
|
33
|
+
forEach(serviceDescriptors, (method) => {
|
|
34
|
+
method.requestStream = true;
|
|
35
|
+
method.responseStream = true;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (noSerialise || voltId) {
|
|
39
|
+
if (voltId && !serviceId) {
|
|
40
|
+
// We must have a service identifier id when tunnelling in order to be able to route the rpc.
|
|
41
|
+
throw new Error("createClient - no serviceId given");
|
|
42
|
+
}
|
|
43
|
+
forEach(serviceDescriptors, (method) => {
|
|
44
|
+
if (voltId) {
|
|
45
|
+
// Prepend the resource id to the method path if tunnelling.
|
|
46
|
+
if (voltId === serviceId) {
|
|
47
|
+
method.path = `/${voltId}${method.path}`;
|
|
48
|
+
} else {
|
|
49
|
+
method.path = `/${voltId}/${serviceId}${method.path}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (noSerialise) {
|
|
53
|
+
// Save original serialisation methods so we can encrypt/decrypt in the tunnel.
|
|
54
|
+
method.requestSerializeOriginal = method.requestSerialize;
|
|
55
|
+
method.responseDeserializeOriginal = method.responseDeserialize;
|
|
56
|
+
|
|
57
|
+
// Replace default serialisation with NOP, since when tunnelling we encrypt the payload
|
|
58
|
+
// before sending it to the channel, so we want it to be treated as a pure byte array (pass-through).
|
|
59
|
+
method.requestSerialize = serialiseNOP;
|
|
60
|
+
method.responseDeserialize = serialiseNOP;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const SubClass = grpc.makeGenericClientConstructor(serviceDescriptors);
|
|
66
|
+
|
|
67
|
+
return new SubClass(address, credentials);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Implementations should utilise promises => wrap each method as a grpc request/response callback.
|
|
72
|
+
* @param {*} api
|
|
73
|
+
*/
|
|
74
|
+
const grpcWrap = (api) => {
|
|
75
|
+
const grpcRoutes = {};
|
|
76
|
+
|
|
77
|
+
// Don't bother trying to wrap class constructors.
|
|
78
|
+
const ignoreMethods = ["constructor"];
|
|
79
|
+
|
|
80
|
+
// Support class instances and PoJos
|
|
81
|
+
const isClass = api.constructor !== Object;
|
|
82
|
+
const iterate = Object.getOwnPropertyNames(isClass ? api.constructor.prototype : api);
|
|
83
|
+
|
|
84
|
+
// Wrap each method on the api.
|
|
85
|
+
forEach(iterate, (name) => {
|
|
86
|
+
if (!ignoreMethods.includes(name)) {
|
|
87
|
+
const method = api[name];
|
|
88
|
+
grpcRoutes[name] = (call, callback) => {
|
|
89
|
+
// Execute the method in the context of the api.
|
|
90
|
+
const resultPromise = method.call(api, call.request, call);
|
|
91
|
+
|
|
92
|
+
// Enforce promise results.
|
|
93
|
+
if (!resultPromise || !resultPromise.then || !resultPromise.catch) {
|
|
94
|
+
throw new Error(`implementation methods must return a Promise [${name}]`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
resultPromise
|
|
98
|
+
.then((response) => {
|
|
99
|
+
if (callback) {
|
|
100
|
+
callback(null, response);
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
.catch((err) => {
|
|
104
|
+
if (callback) {
|
|
105
|
+
callback(err);
|
|
106
|
+
} else {
|
|
107
|
+
// TODO - verify this => how to send error to stream clients?
|
|
108
|
+
// Destroy stream on error.
|
|
109
|
+
call.destroy(err);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return grpcRoutes;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
class GrpcServer {
|
|
120
|
+
constructor(options) {
|
|
121
|
+
const {grpc, serviceDescriptors, routes, tlsOptions, requireClientCert} = options;
|
|
122
|
+
this._grpc = grpc;
|
|
123
|
+
this._server = new this._grpc.Server();
|
|
124
|
+
|
|
125
|
+
if (tlsOptions) {
|
|
126
|
+
const caList = [];
|
|
127
|
+
const certChains = [];
|
|
128
|
+
[].concat(tlsOptions).forEach((tlsOption) => {
|
|
129
|
+
caList.push(tlsOption.ca);
|
|
130
|
+
certChains.push({
|
|
131
|
+
private_key: tlsOption.key,
|
|
132
|
+
cert_chain: tlsOption.cert,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Create credentials from the volt CA and our own pub/priv key.
|
|
137
|
+
this._credentials = grpc.ServerCredentials.createSsl(
|
|
138
|
+
// The root CAs we trust when verifying client certificates.
|
|
139
|
+
Buffer.concat(caList),
|
|
140
|
+
// The certificate chain(s) we present to the client.
|
|
141
|
+
certChains,
|
|
142
|
+
// Flag indicating if we insist on a client certificate.
|
|
143
|
+
requireClientCert
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
this._credentials = grpc.ServerCredentials.createInsecure();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (serviceDescriptors && !isEmpty(routes)) {
|
|
150
|
+
// Enforce decoupling of the service implementation.
|
|
151
|
+
const router = grpcWrap(routes);
|
|
152
|
+
this._server.addService(serviceDescriptors, router);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
close(force = false) {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
if (force) {
|
|
159
|
+
resolve(this._server.forceShutdown());
|
|
160
|
+
} else {
|
|
161
|
+
this._server.tryShutdown(resolve);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
listen(address) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
// If no port is specified, grpc will return the port number assigned.
|
|
169
|
+
this._server.bindAsync(address, this._credentials, (err, port) => {
|
|
170
|
+
if (err) {
|
|
171
|
+
reject(err);
|
|
172
|
+
} else {
|
|
173
|
+
this._port = port;
|
|
174
|
+
this._server.start();
|
|
175
|
+
resolve(port);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
get server() {
|
|
182
|
+
return this._server;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get port() {
|
|
186
|
+
return this._port;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createServer(grpc, serviceDescriptors, routes, cryptoCache, requireClientCert) {
|
|
191
|
+
let tlsOptions;
|
|
192
|
+
if (cryptoCache) {
|
|
193
|
+
tlsOptions = createSecureContextOptions(cryptoCache);
|
|
194
|
+
}
|
|
195
|
+
const serverArgs = {grpc, serviceDescriptors, routes, tlsOptions, requireClientCert};
|
|
196
|
+
return new GrpcServer(serverArgs);
|
|
197
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export {constants} from "./constants.js";
|
|
2
|
+
export * as grpcUtils from "./grpc-utils.js";
|
|
3
|
+
export * as utils from "./utils.js";
|
|
4
|
+
export {VoltClient} from "./volt-client.js";
|
|
5
|
+
export * from "./volt-crypto.js";
|
|
6
|
+
export * as protoUtils from "./proto-utils.js";
|
|
7
|
+
export {v4 as generateId} from "uuid";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import protoLoader from "@grpc/proto-loader";
|
|
2
|
+
import lodash from "lodash";
|
|
3
|
+
import {dirname, join} from "path";
|
|
4
|
+
import {fileURLToPath} from "url";
|
|
5
|
+
|
|
6
|
+
const {get: getProp, snakeCase} = lodash;
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
const defaultProtoPath = join(__dirname, "../protobuf");
|
|
10
|
+
|
|
11
|
+
const defaultLoaderOptions = {
|
|
12
|
+
keepCase: true,
|
|
13
|
+
longs: String,
|
|
14
|
+
enums: String,
|
|
15
|
+
defaults: true,
|
|
16
|
+
oneofs: true,
|
|
17
|
+
includeDirs: [defaultProtoPath],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getProtoDescriptors = (grpc, protoPath, opts) => {
|
|
21
|
+
const definition = protoLoader.loadSync(protoPath, opts || defaultLoaderOptions);
|
|
22
|
+
const descriptors = grpc.loadPackageDefinition(definition);
|
|
23
|
+
return descriptors;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getProtoService = (descriptors, servicePath) => {
|
|
27
|
+
const serviceDef = getProp(descriptors, servicePath);
|
|
28
|
+
if (serviceDef && serviceDef.service) {
|
|
29
|
+
return serviceDef.service;
|
|
30
|
+
} else {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function getServiceDescriptorsFromPath(grpc, protoPath, servicePackageName, opts) {
|
|
36
|
+
let serviceDescriptors;
|
|
37
|
+
const descriptors = getProtoDescriptors(grpc, protoPath, opts);
|
|
38
|
+
if (descriptors) {
|
|
39
|
+
serviceDescriptors = getProtoService(descriptors, servicePackageName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return serviceDescriptors;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getServiceDescriptors(grpc, servicePackageName, opts) {
|
|
46
|
+
// Extract the package components of the service proto package. The package name should be of the
|
|
47
|
+
// fully qualified proto path, e.g. tdx.volt_api.webcam.v1.WebcamControlAPI.
|
|
48
|
+
const protoServiceComponents = servicePackageName.split(".");
|
|
49
|
+
|
|
50
|
+
// The service name is the last component of the path.
|
|
51
|
+
const protoServiceName = protoServiceComponents.pop();
|
|
52
|
+
|
|
53
|
+
// The actual file name should match the snake case of the service name.
|
|
54
|
+
const protoFileName = snakeCase(protoServiceName).toLowerCase() + ".proto";
|
|
55
|
+
|
|
56
|
+
// The path to the proto file should match each component of the package name.
|
|
57
|
+
const protoPath = join(defaultProtoPath, ...protoServiceComponents, protoFileName);
|
|
58
|
+
|
|
59
|
+
let serviceDescriptors;
|
|
60
|
+
const descriptors = getProtoDescriptors(grpc, protoPath, opts);
|
|
61
|
+
if (descriptors) {
|
|
62
|
+
serviceDescriptors = getProtoService(descriptors, servicePackageName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return serviceDescriptors;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getProtoDescriptorMethods(protoDescriptor) {
|
|
69
|
+
const methods = [];
|
|
70
|
+
Object.keys(protoDescriptor).forEach((methodName) => {
|
|
71
|
+
const method = protoDescriptor[methodName];
|
|
72
|
+
methods.push({
|
|
73
|
+
path: method.path,
|
|
74
|
+
client_streaming: method.requestStream,
|
|
75
|
+
server_streaming: method.responseStream,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return methods;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {getServiceDescriptors, getProtoDescriptors, getProtoDescriptorMethods, getServiceDescriptorsFromPath};
|
package/src/tunnel.pb.js
ADDED
|
File without changes
|