@microsoft/dev-tunnels-management 1.0.8 → 1.0.7303

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.
@@ -1,574 +1,574 @@
1
- "use strict";
2
- // Copyright (c) Microsoft Corporation.
3
- // Licensed under the MIT license.
4
- Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.TunnelManagementHttpClient = void 0;
6
- const dev_tunnels_contracts_1 = require("@microsoft/dev-tunnels-contracts");
7
- const tunnelManagementClient_1 = require("./tunnelManagementClient");
8
- const tunnelAccessTokenProperties_1 = require("./tunnelAccessTokenProperties");
9
- const version_1 = require("./version");
10
- const axios_1 = require("axios");
11
- const apiV1Path = `/api/v1`;
12
- const tunnelsApiPath = apiV1Path + '/tunnels';
13
- const endpointsApiSubPath = '/endpoints';
14
- const portsApiSubPath = '/ports';
15
- const clustersApiPath = apiV1Path + '/clusters';
16
- const tunnelAuthentication = 'Authorization';
17
- function comparePorts(a, b) {
18
- var _a, _b;
19
- return ((_a = a.portNumber) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER) - ((_b = b.portNumber) !== null && _b !== void 0 ? _b : Number.MAX_SAFE_INTEGER);
20
- }
21
- function parseDate(value) {
22
- return typeof value === 'string' ? new Date(Date.parse(value)) : value;
23
- }
24
- /**
25
- * Fixes Tunnel properties of type Date that were deserialized as strings.
26
- */
27
- function parseTunnelDates(tunnel) {
28
- if (!tunnel)
29
- return;
30
- tunnel.created = parseDate(tunnel.created);
31
- if (tunnel.status) {
32
- tunnel.status.lastHostConnectionTime = parseDate(tunnel.status.lastHostConnectionTime);
33
- tunnel.status.lastClientConnectionTime = parseDate(tunnel.status.lastClientConnectionTime);
34
- }
35
- }
36
- /**
37
- * Fixes TunnelPort properties of type Date that were deserialized as strings.
38
- */
39
- function parseTunnelPortDates(port) {
40
- if (!port)
41
- return;
42
- if (port.status) {
43
- port.status.lastClientConnectionTime = parseDate(port.status.lastClientConnectionTime);
44
- }
45
- }
46
- const manageAccessTokenScope = [dev_tunnels_contracts_1.TunnelAccessScopes.Manage];
47
- const hostAccessTokenScope = [dev_tunnels_contracts_1.TunnelAccessScopes.Host];
48
- const managePortsAccessTokenScopes = [
49
- dev_tunnels_contracts_1.TunnelAccessScopes.Manage,
50
- dev_tunnels_contracts_1.TunnelAccessScopes.ManagePorts,
51
- dev_tunnels_contracts_1.TunnelAccessScopes.Host,
52
- ];
53
- const readAccessTokenScopes = [
54
- dev_tunnels_contracts_1.TunnelAccessScopes.Manage,
55
- dev_tunnels_contracts_1.TunnelAccessScopes.ManagePorts,
56
- dev_tunnels_contracts_1.TunnelAccessScopes.Host,
57
- dev_tunnels_contracts_1.TunnelAccessScopes.Connect,
58
- ];
59
- class TunnelManagementHttpClient {
60
- /**
61
- * Initializes a new instance of the `TunnelManagementHttpClient` class
62
- * with a client authentication callback, service URI, and HTTP handler.
63
- *
64
- * @param userAgent { name, version } object or a comment string to use as the User-Agent header.
65
- * @param userTokenCallback Optional async callback for retrieving a client authentication
66
- * header value with access token, for AAD or GitHub user authentication. This may be omitted
67
- * for anonymous tunnel clients, or if tunnel access tokens will be specified via
68
- * `TunnelRequestOptions.accessToken`.
69
- * @param tunnelServiceUri Optional tunnel service URI (not including any path). Defaults to
70
- * the global tunnel service URI.
71
- * @param httpsAgent Optional agent that will be invoked for HTTPS requests to the tunnel
72
- * service.
73
- */
74
- constructor(userAgents, userTokenCallback, tunnelServiceUri, httpsAgent) {
75
- var _a;
76
- this.httpsAgent = httpsAgent;
77
- this.trace = (msg) => { };
78
- if (!userAgents) {
79
- throw new TypeError('User agent must be provided.');
80
- }
81
- if (Array.isArray(userAgents)) {
82
- if (userAgents.length === 0) {
83
- throw new TypeError('User agents cannot be empty.');
84
- }
85
- let combinedUserAgents = '';
86
- userAgents.forEach((userAgent) => {
87
- var _a;
88
- if (typeof userAgent !== 'string') {
89
- if (!userAgent.name) {
90
- throw new TypeError('Invalid user agent. The name must be provided.');
91
- }
92
- if (typeof userAgent.name !== 'string') {
93
- throw new TypeError('Invalid user agent. The name must be a string.');
94
- }
95
- if (userAgent.version && typeof userAgent.version !== 'string') {
96
- throw new TypeError('Invalid user agent. The version must be a string.');
97
- }
98
- combinedUserAgents = `${combinedUserAgents}${userAgent.name}/${(_a = userAgent.version) !== null && _a !== void 0 ? _a : 'unknown'} `;
99
- }
100
- else {
101
- combinedUserAgents = `${combinedUserAgents}${userAgent} `;
102
- }
103
- });
104
- this.userAgents = combinedUserAgents.trim();
105
- }
106
- else if (typeof userAgents !== 'string') {
107
- if (!userAgents.name) {
108
- throw new TypeError('Invalid user agent. The name must be provided.');
109
- }
110
- if (typeof userAgents.name !== 'string') {
111
- throw new TypeError('Invalid user agent. The name must be a string.');
112
- }
113
- if (userAgents.version && typeof userAgents.version !== 'string') {
114
- throw new TypeError('Invalid user agent. The version must be a string.');
115
- }
116
- this.userAgents = `${userAgents.name}/${(_a = userAgents.version) !== null && _a !== void 0 ? _a : 'unknown'}`;
117
- }
118
- else {
119
- this.userAgents = userAgents;
120
- }
121
- this.userTokenCallback = userTokenCallback !== null && userTokenCallback !== void 0 ? userTokenCallback : (() => Promise.resolve(null));
122
- if (!tunnelServiceUri) {
123
- tunnelServiceUri = dev_tunnels_contracts_1.TunnelServiceProperties.production.serviceUri;
124
- }
125
- const parsedUri = new URL(tunnelServiceUri);
126
- if (!parsedUri || parsedUri.pathname !== '/') {
127
- throw new TypeError(`Invalid tunnel service URI: ${tunnelServiceUri}`);
128
- }
129
- this.baseAddress = tunnelServiceUri;
130
- }
131
- async listTunnels(clusterId, domain, options) {
132
- const queryParams = [clusterId ? null : 'global=true', domain ? `domain=${domain}` : null];
133
- const query = queryParams.filter((p) => !!p).join('&');
134
- const results = (await this.sendRequest('GET', clusterId, tunnelsApiPath, query, options));
135
- results.forEach(parseTunnelDates);
136
- return results;
137
- }
138
- async getTunnel(tunnel, options) {
139
- const result = await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, undefined, undefined, options);
140
- parseTunnelDates(result);
141
- return result;
142
- }
143
- async createTunnel(tunnel, options) {
144
- let tunnelId = tunnel.tunnelId;
145
- if (tunnelId) {
146
- throw new Error('An ID may not be specified when creating a tunnel.');
147
- }
148
- tunnel = this.convertTunnelForRequest(tunnel);
149
- const result = (await this.sendRequest('POST', tunnel.clusterId, tunnelsApiPath, undefined, options, tunnel));
150
- parseTunnelDates(result);
151
- return result;
152
- }
153
- async updateTunnel(tunnel, options) {
154
- const result = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel)));
155
- if (!(options === null || options === void 0 ? void 0 : options.tokenScopes)) {
156
- // If no new tokens were requested in the update, preserve any existing
157
- // access tokens in the resulting tunnel object.
158
- result.accessTokens = tunnel.accessTokens;
159
- }
160
- parseTunnelDates(result);
161
- return result;
162
- }
163
- async deleteTunnel(tunnel, options) {
164
- return await this.sendTunnelRequest('DELETE', tunnel, manageAccessTokenScope, undefined, undefined, options, undefined, true);
165
- }
166
- async updateTunnelEndpoint(tunnel, endpoint, options) {
167
- const path = `${endpointsApiSubPath}/${endpoint.hostId}/${endpoint.connectionMode}`;
168
- const result = (await this.sendTunnelRequest('PUT', tunnel, hostAccessTokenScope, path, undefined, options, endpoint));
169
- if (tunnel.endpoints) {
170
- // Also update the endpoint in the local tunnel object.
171
- tunnel.endpoints = tunnel.endpoints
172
- .filter((e) => e.hostId !== endpoint.hostId ||
173
- e.connectionMode !== endpoint.connectionMode)
174
- .concat(result);
175
- }
176
- return result;
177
- }
178
- async deleteTunnelEndpoints(tunnel, hostId, connectionMode, options) {
179
- const path = connectionMode == null
180
- ? `${endpointsApiSubPath}/${hostId}`
181
- : `${endpointsApiSubPath}/${hostId}/${connectionMode}`;
182
- const result = await this.sendTunnelRequest('DELETE', tunnel, hostAccessTokenScope, path, undefined, options, undefined, true);
183
- if (result && tunnel.endpoints) {
184
- // Also delete the endpoint in the local tunnel object.
185
- tunnel.endpoints = tunnel.endpoints.filter((e) => e.hostId !== hostId || e.connectionMode !== connectionMode);
186
- }
187
- return result;
188
- }
189
- async listTunnelPorts(tunnel, options) {
190
- const results = (await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, portsApiSubPath, undefined, options));
191
- results.forEach(parseTunnelPortDates);
192
- return results;
193
- }
194
- async getTunnelPort(tunnel, portNumber, options) {
195
- const path = `${portsApiSubPath}/${portNumber}`;
196
- const result = await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, path, undefined, options);
197
- parseTunnelPortDates(result);
198
- return result;
199
- }
200
- async createTunnelPort(tunnel, tunnelPort, options) {
201
- tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
202
- const result = (await this.sendTunnelRequest('POST', tunnel, managePortsAccessTokenScopes, portsApiSubPath, undefined, options, tunnelPort));
203
- if (tunnel.ports) {
204
- // Also add the port to the local tunnel object.
205
- tunnel.ports = tunnel.ports
206
- .filter((p) => p.portNumber !== tunnelPort.portNumber)
207
- .concat(result)
208
- .sort(comparePorts);
209
- }
210
- parseTunnelPortDates(result);
211
- return result;
212
- }
213
- async updateTunnelPort(tunnel, tunnelPort, options) {
214
- if (tunnelPort.clusterId && tunnel.clusterId && tunnelPort.clusterId !== tunnel.clusterId) {
215
- throw new Error('Tunnel port cluster ID is not consistent.');
216
- }
217
- const portNumber = tunnelPort.portNumber;
218
- const path = `${portsApiSubPath}/${portNumber}`;
219
- tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
220
- const result = (await this.sendTunnelRequest('PUT', tunnel, managePortsAccessTokenScopes, path, undefined, options, tunnelPort));
221
- if (tunnel.ports) {
222
- // Also update the port in the local tunnel object.
223
- tunnel.ports = tunnel.ports
224
- .filter((p) => p.portNumber !== tunnelPort.portNumber)
225
- .concat(result)
226
- .sort(comparePorts);
227
- }
228
- if (!(options === null || options === void 0 ? void 0 : options.tokenScopes)) {
229
- // If no new tokens were requested in the update, preserve any existing
230
- // access tokens in the resulting port object.
231
- result.accessTokens = tunnelPort.accessTokens;
232
- }
233
- parseTunnelPortDates(result);
234
- return result;
235
- }
236
- async deleteTunnelPort(tunnel, portNumber, options) {
237
- const path = `${portsApiSubPath}/${portNumber}`;
238
- const result = await this.sendTunnelRequest('DELETE', tunnel, managePortsAccessTokenScopes, path, undefined, options, undefined, true);
239
- if (result && tunnel.ports) {
240
- // Also delete the port in the local tunnel object.
241
- tunnel.ports = tunnel.ports
242
- .filter((p) => p.portNumber !== portNumber)
243
- .sort(comparePorts);
244
- }
245
- return result;
246
- }
247
- async listClusters() {
248
- return (await this.sendRequest('GET', undefined, clustersApiPath, undefined, undefined, undefined, false));
249
- }
250
- /**
251
- * Sends an HTTP request to the tunnel management API, targeting a specific tunnel.
252
- * This protected method enables subclasses to support additional tunnel management APIs.
253
- * @param method HTTP request method.
254
- * @param tunnel Tunnel that the request is targeting.
255
- * @param accessTokenScopes Required array of access scopes for tokens in `tunnel.accessTokens`
256
- * that could be used to authorize the request.
257
- * @param path Optional request sub-path relative to the tunnel.
258
- * @param query Optional query string to append to the request.
259
- * @param options Request options.
260
- * @param body Optional request body object.
261
- * @param allowNotFound If true, a 404 response is returned as a null or false result
262
- * instead of an error.
263
- * @returns Result of the request.
264
- */
265
- async sendTunnelRequest(method, tunnel, accessTokenScopes, path, query, options, body, allowNotFound) {
266
- const uri = this.buildUriForTunnel(tunnel, path, query, options);
267
- const config = await this.getAxiosRequestConfig(tunnel, options, accessTokenScopes);
268
- const result = await this.request(method, uri, body, config, allowNotFound);
269
- return result;
270
- }
271
- /**
272
- * Sends an HTTP request to the tunnel management API.
273
- * This protected method enables subclasses to support additional tunnel management APIs.
274
- * @param method HTTP request method.
275
- * @param clusterId Optional tunnel service cluster ID to direct the request to. If unspecified,
276
- * the request will use the global traffic-manager to find the nearest cluster.
277
- * @param path Required request path.
278
- * @param query Optional query string to append to the request.
279
- * @param options Request options.
280
- * @param body Optional request body object.
281
- * @param allowNotFound If true, a 404 response is returned as a null or false result
282
- * instead of an error.
283
- * @returns Result of the request.
284
- */
285
- async sendRequest(method, clusterId, path, query, options, body, allowNotFound) {
286
- const uri = this.buildUri(clusterId, path, query, options);
287
- const config = await this.getAxiosRequestConfig(undefined, options);
288
- const result = await this.request(method, uri, body, config, allowNotFound);
289
- return result;
290
- }
291
- getResponseErrorMessage(error) {
292
- var _a, _b, _c, _d;
293
- let errorMessage = '';
294
- if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
295
- let problemDetails = error.response.data;
296
- if (problemDetails.title || problemDetails.detail) {
297
- errorMessage = `Tunnel service error: ${problemDetails.title}`;
298
- if (problemDetails.detail) {
299
- errorMessage += ' ' + problemDetails.detail;
300
- }
301
- if (problemDetails.errors) {
302
- errorMessage += JSON.stringify(problemDetails.errors);
303
- }
304
- }
305
- }
306
- if (!errorMessage) {
307
- if (error === null || error === void 0 ? void 0 : error.response) {
308
- errorMessage =
309
- 'Tunnel service returned status code: ' +
310
- `${error.response.status} ${error.response.statusText}`;
311
- }
312
- else {
313
- errorMessage = (_c = (_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : error) !== null && _c !== void 0 ? _c : 'Unknown tunnel service request error.';
314
- }
315
- }
316
- const requestIdHeaderName = 'VsSaaS-Request-Id';
317
- if (((_d = error.response) === null || _d === void 0 ? void 0 : _d.headers) && error.response.headers[requestIdHeaderName]) {
318
- errorMessage += `\nRequest ID: ${error.response.headers[requestIdHeaderName]}`;
319
- }
320
- return errorMessage;
321
- }
322
- // Helper functions
323
- buildUri(clusterId, path, query, options) {
324
- let baseAddress = this.baseAddress;
325
- if (clusterId) {
326
- const url = new URL(baseAddress);
327
- const portNumber = parseInt(url.port, 10);
328
- if (url.hostname !== 'localhost' && !url.hostname.startsWith(`${clusterId}.`)) {
329
- // A specific cluster ID was specified (while not running on localhost).
330
- // Prepend the cluster ID to the hostname, and optionally strip a global prefix.
331
- url.hostname = `${clusterId}.${url.hostname}`.replace('global.', '');
332
- baseAddress = url.toString();
333
- }
334
- else if (url.protocol === 'https:' &&
335
- clusterId.startsWith('localhost') &&
336
- portNumber % 10 > 0) {
337
- // Local testing simulates clusters by running the service on multiple ports.
338
- // Change the port number to match the cluster ID suffix.
339
- const clusterNumber = parseInt(clusterId.substring('localhost'.length), 10);
340
- if (clusterNumber > 0 && clusterNumber < 10) {
341
- url.port = (portNumber - (portNumber % 10) + clusterNumber).toString();
342
- baseAddress = url.toString();
343
- }
344
- }
345
- }
346
- baseAddress = `${baseAddress.replace(/\/$/, '')}${path}`;
347
- let optionsQuery = this.tunnelRequestOptionsToQueryString(options, query);
348
- if (optionsQuery) {
349
- baseAddress += `?${optionsQuery}`;
350
- }
351
- return baseAddress;
352
- }
353
- buildUriForTunnel(tunnel, path, query, options) {
354
- let tunnelPath = '';
355
- if (tunnel.clusterId && tunnel.tunnelId) {
356
- tunnelPath = `${tunnelsApiPath}/${tunnel.tunnelId}`;
357
- }
358
- else {
359
- if (!tunnel.name) {
360
- throw new Error('Tunnel object must include either a name or tunnel ID and cluster ID.');
361
- }
362
- tunnelPath = `${tunnelsApiPath}/${tunnel.name}`;
363
- }
364
- if (options === null || options === void 0 ? void 0 : options.additionalQueryParameters) {
365
- for (let [paramName, paramValue] of Object.entries(options.additionalQueryParameters)) {
366
- if (query) {
367
- query += `&${paramName}=${paramValue}`;
368
- }
369
- else {
370
- query = `${paramName}=${paramValue}`;
371
- }
372
- }
373
- }
374
- return this.buildUri(tunnel.clusterId, tunnelPath + (path ? path : ''), query, options);
375
- }
376
- async getAxiosRequestConfig(tunnel, options, accessTokenScopes) {
377
- // Get access token header
378
- let headers = {};
379
- if (options && options.accessToken) {
380
- tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.validateTokenExpiration(options.accessToken);
381
- headers[tunnelAuthentication] = `${tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel} ${options.accessToken}`;
382
- }
383
- if (!(tunnelAuthentication in headers) && this.userTokenCallback) {
384
- let token = await this.userTokenCallback();
385
- if (token) {
386
- headers[tunnelAuthentication] = token;
387
- }
388
- }
389
- if (!(tunnelAuthentication in headers)) {
390
- const accessToken = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.getTunnelAccessToken(tunnel, accessTokenScopes);
391
- if (accessToken) {
392
- headers[tunnelAuthentication] = `${tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel} ${accessToken}`;
393
- }
394
- }
395
- const copyAdditionalHeaders = (additionalHeaders) => {
396
- if (additionalHeaders) {
397
- for (let [headerName, headerValue] of Object.entries(additionalHeaders)) {
398
- headers[headerName] = headerValue;
399
- }
400
- }
401
- };
402
- copyAdditionalHeaders(this.additionalRequestHeaders);
403
- copyAdditionalHeaders(options === null || options === void 0 ? void 0 : options.additionalHeaders);
404
- const userAgentPrefix = headers['User-Agent'] ? headers['User-Agent'] + ' ' : '';
405
- headers['User-Agent'] = `${userAgentPrefix}${this.userAgents} ${version_1.tunnelSdkUserAgent}`;
406
- // Get axios config
407
- let config = Object.assign({ headers }, (this.httpsAgent && { httpsAgent: this.httpsAgent }));
408
- if ((options === null || options === void 0 ? void 0 : options.followRedirects) === false) {
409
- config.maxRedirects = 0;
410
- }
411
- return config;
412
- }
413
- convertTunnelForRequest(tunnel) {
414
- var _a;
415
- const convertedTunnel = {
416
- name: tunnel.name,
417
- domain: tunnel.domain,
418
- description: tunnel.description,
419
- tags: tunnel.tags,
420
- options: tunnel.options,
421
- accessControl: !tunnel.accessControl
422
- ? undefined
423
- : { entries: tunnel.accessControl.entries.filter((ace) => !ace.isInherited) },
424
- endpoints: tunnel.endpoints,
425
- ports: (_a = tunnel.ports) === null || _a === void 0 ? void 0 : _a.map((p) => this.convertTunnelPortForRequest(tunnel, p)),
426
- };
427
- return convertedTunnel;
428
- }
429
- convertTunnelPortForRequest(tunnel, tunnelPort) {
430
- if (tunnelPort.clusterId && tunnel.clusterId && tunnelPort.clusterId !== tunnel.clusterId) {
431
- throw new Error('Tunnel port cluster ID does not match tunnel.');
432
- }
433
- if (tunnelPort.tunnelId && tunnel.tunnelId && tunnelPort.tunnelId !== tunnel.tunnelId) {
434
- throw new Error('Tunnel port tunnel ID does not match tunnel.');
435
- }
436
- return {
437
- portNumber: tunnelPort.portNumber,
438
- protocol: tunnelPort.protocol,
439
- options: tunnelPort.options,
440
- accessControl: !tunnelPort.accessControl
441
- ? undefined
442
- : { entries: tunnelPort.accessControl.entries.filter((ace) => !ace.isInherited) },
443
- };
444
- }
445
- tunnelRequestOptionsToQueryString(options, additionalQuery) {
446
- let queryOptions = {};
447
- const queryItems = [];
448
- if (options) {
449
- if (options.includePorts) {
450
- queryOptions['includePorts'] = ['true'];
451
- }
452
- if (options.tokenScopes) {
453
- dev_tunnels_contracts_1.TunnelAccessControl.validateScopes(options.tokenScopes, undefined, true);
454
- queryOptions['tokenScopes'] = options.tokenScopes;
455
- }
456
- if (options.forceRename) {
457
- queryOptions['forceRename'] = ['true'];
458
- }
459
- if (options.tags) {
460
- queryOptions['tags'] = options.tags;
461
- if (options.requireAllTags) {
462
- queryOptions['allTags'] = ['true'];
463
- }
464
- }
465
- queryItems.push(...Object.keys(queryOptions).map((key) => {
466
- const value = queryOptions[key];
467
- return `${key}=${value.map(encodeURIComponent).join(',')}`;
468
- }));
469
- }
470
- if (additionalQuery) {
471
- queryItems.push(additionalQuery);
472
- }
473
- const queryString = queryItems.join('&');
474
- return queryString;
475
- }
476
- /**
477
- * Makes an HTTP request using Axios, while tracing request and response details.
478
- */
479
- async request(method, uri, data, config, allowNotFound) {
480
- this.trace(`${method} ${uri}`);
481
- this.traceHeaders(config.headers);
482
- this.traceContent(data);
483
- const traceResponse = (response) => {
484
- this.trace(`${response.status} ${response.statusText}`);
485
- this.traceHeaders(response.headers);
486
- this.traceContent(response.data);
487
- };
488
- try {
489
- config.url = uri;
490
- config.method = method;
491
- config.data = data;
492
- let response = await axios_1.default.request(config);
493
- traceResponse(response);
494
- // This assumes that TResult is always boolean for DELETE requests.
495
- return (method === 'DELETE' ? true : response.data);
496
- }
497
- catch (e) {
498
- if (!(e instanceof Error) || !e.isAxiosError)
499
- throw e;
500
- const requestError = e;
501
- if (requestError.response) {
502
- traceResponse(requestError.response);
503
- if (allowNotFound && requestError.response.status === 404) {
504
- return (method === 'DELETE' ? false : null);
505
- }
506
- }
507
- requestError.message = this.getResponseErrorMessage(requestError);
508
- // Axios errors have too much redundant detail! Delete some of it.
509
- delete requestError.request;
510
- if (requestError.response) {
511
- delete requestError.config.httpAgent;
512
- delete requestError.config.httpsAgent;
513
- delete requestError.response.request;
514
- }
515
- throw requestError;
516
- }
517
- }
518
- traceHeaders(headers) {
519
- for (let [headerName, headerValue] of Object.entries(headers)) {
520
- if (headerName === 'Authorization') {
521
- this.traceAuthorizationHeader(headerName, headerValue);
522
- return;
523
- }
524
- this.trace(`${headerName}: ${headerValue !== null && headerValue !== void 0 ? headerValue : ''}`);
525
- }
526
- }
527
- traceAuthorizationHeader(key, value) {
528
- if (typeof value !== 'string')
529
- return;
530
- const spaceIndex = value.indexOf(' ');
531
- if (spaceIndex < 0) {
532
- this.trace(`${key}: [${value.length}]`);
533
- return;
534
- }
535
- const scheme = value.substring(0, spaceIndex);
536
- const token = value.substring(spaceIndex + 1);
537
- if (scheme.toLowerCase() === tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel.toLowerCase()) {
538
- const tokenProperties = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.tryParse(token);
539
- if (tokenProperties) {
540
- this.trace(`${key}: ${scheme} <${tokenProperties}>`);
541
- return;
542
- }
543
- }
544
- this.trace(`${key}: ${scheme} <token>`);
545
- }
546
- traceContent(data) {
547
- if (typeof data === 'object') {
548
- data = JSON.stringify(data, undefined, ' ');
549
- }
550
- if (typeof data === 'string') {
551
- this.trace(TunnelManagementHttpClient.replaceTokensInContent(data));
552
- }
553
- }
554
- static replaceTokensInContent(content) {
555
- var _a;
556
- const tokenRegex = /"(eyJ[a-zA-z0-9\-_]+\.[a-zA-z0-9\-_]+\.[a-zA-z0-9\-_]+)"/;
557
- let match = tokenRegex.exec(content);
558
- while (match) {
559
- let token = match[1];
560
- const tokenProperties = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.tryParse(token);
561
- token = (_a = tokenProperties === null || tokenProperties === void 0 ? void 0 : tokenProperties.toString()) !== null && _a !== void 0 ? _a : 'token';
562
- content =
563
- content.substring(0, match.index + 1) +
564
- '<' +
565
- token +
566
- '>' +
567
- content.substring(match.index + match[0].length - 1);
568
- match = tokenRegex.exec(content);
569
- }
570
- return content;
571
- }
572
- }
573
- exports.TunnelManagementHttpClient = TunnelManagementHttpClient;
1
+ "use strict";
2
+ // Copyright (c) Microsoft Corporation.
3
+ // Licensed under the MIT license.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.TunnelManagementHttpClient = void 0;
6
+ const dev_tunnels_contracts_1 = require("@microsoft/dev-tunnels-contracts");
7
+ const tunnelManagementClient_1 = require("./tunnelManagementClient");
8
+ const tunnelAccessTokenProperties_1 = require("./tunnelAccessTokenProperties");
9
+ const version_1 = require("./version");
10
+ const axios_1 = require("axios");
11
+ const apiV1Path = `/api/v1`;
12
+ const tunnelsApiPath = apiV1Path + '/tunnels';
13
+ const endpointsApiSubPath = '/endpoints';
14
+ const portsApiSubPath = '/ports';
15
+ const clustersApiPath = apiV1Path + '/clusters';
16
+ const tunnelAuthentication = 'Authorization';
17
+ function comparePorts(a, b) {
18
+ var _a, _b;
19
+ return ((_a = a.portNumber) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER) - ((_b = b.portNumber) !== null && _b !== void 0 ? _b : Number.MAX_SAFE_INTEGER);
20
+ }
21
+ function parseDate(value) {
22
+ return typeof value === 'string' ? new Date(Date.parse(value)) : value;
23
+ }
24
+ /**
25
+ * Fixes Tunnel properties of type Date that were deserialized as strings.
26
+ */
27
+ function parseTunnelDates(tunnel) {
28
+ if (!tunnel)
29
+ return;
30
+ tunnel.created = parseDate(tunnel.created);
31
+ if (tunnel.status) {
32
+ tunnel.status.lastHostConnectionTime = parseDate(tunnel.status.lastHostConnectionTime);
33
+ tunnel.status.lastClientConnectionTime = parseDate(tunnel.status.lastClientConnectionTime);
34
+ }
35
+ }
36
+ /**
37
+ * Fixes TunnelPort properties of type Date that were deserialized as strings.
38
+ */
39
+ function parseTunnelPortDates(port) {
40
+ if (!port)
41
+ return;
42
+ if (port.status) {
43
+ port.status.lastClientConnectionTime = parseDate(port.status.lastClientConnectionTime);
44
+ }
45
+ }
46
+ const manageAccessTokenScope = [dev_tunnels_contracts_1.TunnelAccessScopes.Manage];
47
+ const hostAccessTokenScope = [dev_tunnels_contracts_1.TunnelAccessScopes.Host];
48
+ const managePortsAccessTokenScopes = [
49
+ dev_tunnels_contracts_1.TunnelAccessScopes.Manage,
50
+ dev_tunnels_contracts_1.TunnelAccessScopes.ManagePorts,
51
+ dev_tunnels_contracts_1.TunnelAccessScopes.Host,
52
+ ];
53
+ const readAccessTokenScopes = [
54
+ dev_tunnels_contracts_1.TunnelAccessScopes.Manage,
55
+ dev_tunnels_contracts_1.TunnelAccessScopes.ManagePorts,
56
+ dev_tunnels_contracts_1.TunnelAccessScopes.Host,
57
+ dev_tunnels_contracts_1.TunnelAccessScopes.Connect,
58
+ ];
59
+ class TunnelManagementHttpClient {
60
+ /**
61
+ * Initializes a new instance of the `TunnelManagementHttpClient` class
62
+ * with a client authentication callback, service URI, and HTTP handler.
63
+ *
64
+ * @param userAgent { name, version } object or a comment string to use as the User-Agent header.
65
+ * @param userTokenCallback Optional async callback for retrieving a client authentication
66
+ * header value with access token, for AAD or GitHub user authentication. This may be omitted
67
+ * for anonymous tunnel clients, or if tunnel access tokens will be specified via
68
+ * `TunnelRequestOptions.accessToken`.
69
+ * @param tunnelServiceUri Optional tunnel service URI (not including any path). Defaults to
70
+ * the global tunnel service URI.
71
+ * @param httpsAgent Optional agent that will be invoked for HTTPS requests to the tunnel
72
+ * service.
73
+ */
74
+ constructor(userAgents, userTokenCallback, tunnelServiceUri, httpsAgent) {
75
+ var _a;
76
+ this.httpsAgent = httpsAgent;
77
+ this.trace = (msg) => { };
78
+ if (!userAgents) {
79
+ throw new TypeError('User agent must be provided.');
80
+ }
81
+ if (Array.isArray(userAgents)) {
82
+ if (userAgents.length === 0) {
83
+ throw new TypeError('User agents cannot be empty.');
84
+ }
85
+ let combinedUserAgents = '';
86
+ userAgents.forEach((userAgent) => {
87
+ var _a;
88
+ if (typeof userAgent !== 'string') {
89
+ if (!userAgent.name) {
90
+ throw new TypeError('Invalid user agent. The name must be provided.');
91
+ }
92
+ if (typeof userAgent.name !== 'string') {
93
+ throw new TypeError('Invalid user agent. The name must be a string.');
94
+ }
95
+ if (userAgent.version && typeof userAgent.version !== 'string') {
96
+ throw new TypeError('Invalid user agent. The version must be a string.');
97
+ }
98
+ combinedUserAgents = `${combinedUserAgents}${userAgent.name}/${(_a = userAgent.version) !== null && _a !== void 0 ? _a : 'unknown'} `;
99
+ }
100
+ else {
101
+ combinedUserAgents = `${combinedUserAgents}${userAgent} `;
102
+ }
103
+ });
104
+ this.userAgents = combinedUserAgents.trim();
105
+ }
106
+ else if (typeof userAgents !== 'string') {
107
+ if (!userAgents.name) {
108
+ throw new TypeError('Invalid user agent. The name must be provided.');
109
+ }
110
+ if (typeof userAgents.name !== 'string') {
111
+ throw new TypeError('Invalid user agent. The name must be a string.');
112
+ }
113
+ if (userAgents.version && typeof userAgents.version !== 'string') {
114
+ throw new TypeError('Invalid user agent. The version must be a string.');
115
+ }
116
+ this.userAgents = `${userAgents.name}/${(_a = userAgents.version) !== null && _a !== void 0 ? _a : 'unknown'}`;
117
+ }
118
+ else {
119
+ this.userAgents = userAgents;
120
+ }
121
+ this.userTokenCallback = userTokenCallback !== null && userTokenCallback !== void 0 ? userTokenCallback : (() => Promise.resolve(null));
122
+ if (!tunnelServiceUri) {
123
+ tunnelServiceUri = dev_tunnels_contracts_1.TunnelServiceProperties.production.serviceUri;
124
+ }
125
+ const parsedUri = new URL(tunnelServiceUri);
126
+ if (!parsedUri || parsedUri.pathname !== '/') {
127
+ throw new TypeError(`Invalid tunnel service URI: ${tunnelServiceUri}`);
128
+ }
129
+ this.baseAddress = tunnelServiceUri;
130
+ }
131
+ async listTunnels(clusterId, domain, options) {
132
+ const queryParams = [clusterId ? null : 'global=true', domain ? `domain=${domain}` : null];
133
+ const query = queryParams.filter((p) => !!p).join('&');
134
+ const results = (await this.sendRequest('GET', clusterId, tunnelsApiPath, query, options));
135
+ results.forEach(parseTunnelDates);
136
+ return results;
137
+ }
138
+ async getTunnel(tunnel, options) {
139
+ const result = await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, undefined, undefined, options);
140
+ parseTunnelDates(result);
141
+ return result;
142
+ }
143
+ async createTunnel(tunnel, options) {
144
+ let tunnelId = tunnel.tunnelId;
145
+ if (tunnelId) {
146
+ throw new Error('An ID may not be specified when creating a tunnel.');
147
+ }
148
+ tunnel = this.convertTunnelForRequest(tunnel);
149
+ const result = (await this.sendRequest('POST', tunnel.clusterId, tunnelsApiPath, undefined, options, tunnel));
150
+ parseTunnelDates(result);
151
+ return result;
152
+ }
153
+ async updateTunnel(tunnel, options) {
154
+ const result = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel)));
155
+ if (!(options === null || options === void 0 ? void 0 : options.tokenScopes)) {
156
+ // If no new tokens were requested in the update, preserve any existing
157
+ // access tokens in the resulting tunnel object.
158
+ result.accessTokens = tunnel.accessTokens;
159
+ }
160
+ parseTunnelDates(result);
161
+ return result;
162
+ }
163
+ async deleteTunnel(tunnel, options) {
164
+ return await this.sendTunnelRequest('DELETE', tunnel, manageAccessTokenScope, undefined, undefined, options, undefined, true);
165
+ }
166
+ async updateTunnelEndpoint(tunnel, endpoint, options) {
167
+ const path = `${endpointsApiSubPath}/${endpoint.hostId}/${endpoint.connectionMode}`;
168
+ const result = (await this.sendTunnelRequest('PUT', tunnel, hostAccessTokenScope, path, undefined, options, endpoint));
169
+ if (tunnel.endpoints) {
170
+ // Also update the endpoint in the local tunnel object.
171
+ tunnel.endpoints = tunnel.endpoints
172
+ .filter((e) => e.hostId !== endpoint.hostId ||
173
+ e.connectionMode !== endpoint.connectionMode)
174
+ .concat(result);
175
+ }
176
+ return result;
177
+ }
178
+ async deleteTunnelEndpoints(tunnel, hostId, connectionMode, options) {
179
+ const path = connectionMode == null
180
+ ? `${endpointsApiSubPath}/${hostId}`
181
+ : `${endpointsApiSubPath}/${hostId}/${connectionMode}`;
182
+ const result = await this.sendTunnelRequest('DELETE', tunnel, hostAccessTokenScope, path, undefined, options, undefined, true);
183
+ if (result && tunnel.endpoints) {
184
+ // Also delete the endpoint in the local tunnel object.
185
+ tunnel.endpoints = tunnel.endpoints.filter((e) => e.hostId !== hostId || e.connectionMode !== connectionMode);
186
+ }
187
+ return result;
188
+ }
189
+ async listTunnelPorts(tunnel, options) {
190
+ const results = (await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, portsApiSubPath, undefined, options));
191
+ results.forEach(parseTunnelPortDates);
192
+ return results;
193
+ }
194
+ async getTunnelPort(tunnel, portNumber, options) {
195
+ const path = `${portsApiSubPath}/${portNumber}`;
196
+ const result = await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, path, undefined, options);
197
+ parseTunnelPortDates(result);
198
+ return result;
199
+ }
200
+ async createTunnelPort(tunnel, tunnelPort, options) {
201
+ tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
202
+ const result = (await this.sendTunnelRequest('POST', tunnel, managePortsAccessTokenScopes, portsApiSubPath, undefined, options, tunnelPort));
203
+ if (tunnel.ports) {
204
+ // Also add the port to the local tunnel object.
205
+ tunnel.ports = tunnel.ports
206
+ .filter((p) => p.portNumber !== tunnelPort.portNumber)
207
+ .concat(result)
208
+ .sort(comparePorts);
209
+ }
210
+ parseTunnelPortDates(result);
211
+ return result;
212
+ }
213
+ async updateTunnelPort(tunnel, tunnelPort, options) {
214
+ if (tunnelPort.clusterId && tunnel.clusterId && tunnelPort.clusterId !== tunnel.clusterId) {
215
+ throw new Error('Tunnel port cluster ID is not consistent.');
216
+ }
217
+ const portNumber = tunnelPort.portNumber;
218
+ const path = `${portsApiSubPath}/${portNumber}`;
219
+ tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
220
+ const result = (await this.sendTunnelRequest('PUT', tunnel, managePortsAccessTokenScopes, path, undefined, options, tunnelPort));
221
+ if (tunnel.ports) {
222
+ // Also update the port in the local tunnel object.
223
+ tunnel.ports = tunnel.ports
224
+ .filter((p) => p.portNumber !== tunnelPort.portNumber)
225
+ .concat(result)
226
+ .sort(comparePorts);
227
+ }
228
+ if (!(options === null || options === void 0 ? void 0 : options.tokenScopes)) {
229
+ // If no new tokens were requested in the update, preserve any existing
230
+ // access tokens in the resulting port object.
231
+ result.accessTokens = tunnelPort.accessTokens;
232
+ }
233
+ parseTunnelPortDates(result);
234
+ return result;
235
+ }
236
+ async deleteTunnelPort(tunnel, portNumber, options) {
237
+ const path = `${portsApiSubPath}/${portNumber}`;
238
+ const result = await this.sendTunnelRequest('DELETE', tunnel, managePortsAccessTokenScopes, path, undefined, options, undefined, true);
239
+ if (result && tunnel.ports) {
240
+ // Also delete the port in the local tunnel object.
241
+ tunnel.ports = tunnel.ports
242
+ .filter((p) => p.portNumber !== portNumber)
243
+ .sort(comparePorts);
244
+ }
245
+ return result;
246
+ }
247
+ async listClusters() {
248
+ return (await this.sendRequest('GET', undefined, clustersApiPath, undefined, undefined, undefined, false));
249
+ }
250
+ /**
251
+ * Sends an HTTP request to the tunnel management API, targeting a specific tunnel.
252
+ * This protected method enables subclasses to support additional tunnel management APIs.
253
+ * @param method HTTP request method.
254
+ * @param tunnel Tunnel that the request is targeting.
255
+ * @param accessTokenScopes Required array of access scopes for tokens in `tunnel.accessTokens`
256
+ * that could be used to authorize the request.
257
+ * @param path Optional request sub-path relative to the tunnel.
258
+ * @param query Optional query string to append to the request.
259
+ * @param options Request options.
260
+ * @param body Optional request body object.
261
+ * @param allowNotFound If true, a 404 response is returned as a null or false result
262
+ * instead of an error.
263
+ * @returns Result of the request.
264
+ */
265
+ async sendTunnelRequest(method, tunnel, accessTokenScopes, path, query, options, body, allowNotFound) {
266
+ const uri = this.buildUriForTunnel(tunnel, path, query, options);
267
+ const config = await this.getAxiosRequestConfig(tunnel, options, accessTokenScopes);
268
+ const result = await this.request(method, uri, body, config, allowNotFound);
269
+ return result;
270
+ }
271
+ /**
272
+ * Sends an HTTP request to the tunnel management API.
273
+ * This protected method enables subclasses to support additional tunnel management APIs.
274
+ * @param method HTTP request method.
275
+ * @param clusterId Optional tunnel service cluster ID to direct the request to. If unspecified,
276
+ * the request will use the global traffic-manager to find the nearest cluster.
277
+ * @param path Required request path.
278
+ * @param query Optional query string to append to the request.
279
+ * @param options Request options.
280
+ * @param body Optional request body object.
281
+ * @param allowNotFound If true, a 404 response is returned as a null or false result
282
+ * instead of an error.
283
+ * @returns Result of the request.
284
+ */
285
+ async sendRequest(method, clusterId, path, query, options, body, allowNotFound) {
286
+ const uri = this.buildUri(clusterId, path, query, options);
287
+ const config = await this.getAxiosRequestConfig(undefined, options);
288
+ const result = await this.request(method, uri, body, config, allowNotFound);
289
+ return result;
290
+ }
291
+ getResponseErrorMessage(error) {
292
+ var _a, _b, _c, _d;
293
+ let errorMessage = '';
294
+ if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
295
+ let problemDetails = error.response.data;
296
+ if (problemDetails.title || problemDetails.detail) {
297
+ errorMessage = `Tunnel service error: ${problemDetails.title}`;
298
+ if (problemDetails.detail) {
299
+ errorMessage += ' ' + problemDetails.detail;
300
+ }
301
+ if (problemDetails.errors) {
302
+ errorMessage += JSON.stringify(problemDetails.errors);
303
+ }
304
+ }
305
+ }
306
+ if (!errorMessage) {
307
+ if (error === null || error === void 0 ? void 0 : error.response) {
308
+ errorMessage =
309
+ 'Tunnel service returned status code: ' +
310
+ `${error.response.status} ${error.response.statusText}`;
311
+ }
312
+ else {
313
+ errorMessage = (_c = (_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : error) !== null && _c !== void 0 ? _c : 'Unknown tunnel service request error.';
314
+ }
315
+ }
316
+ const requestIdHeaderName = 'VsSaaS-Request-Id';
317
+ if (((_d = error.response) === null || _d === void 0 ? void 0 : _d.headers) && error.response.headers[requestIdHeaderName]) {
318
+ errorMessage += `\nRequest ID: ${error.response.headers[requestIdHeaderName]}`;
319
+ }
320
+ return errorMessage;
321
+ }
322
+ // Helper functions
323
+ buildUri(clusterId, path, query, options) {
324
+ let baseAddress = this.baseAddress;
325
+ if (clusterId) {
326
+ const url = new URL(baseAddress);
327
+ const portNumber = parseInt(url.port, 10);
328
+ if (url.hostname !== 'localhost' && !url.hostname.startsWith(`${clusterId}.`)) {
329
+ // A specific cluster ID was specified (while not running on localhost).
330
+ // Prepend the cluster ID to the hostname, and optionally strip a global prefix.
331
+ url.hostname = `${clusterId}.${url.hostname}`.replace('global.', '');
332
+ baseAddress = url.toString();
333
+ }
334
+ else if (url.protocol === 'https:' &&
335
+ clusterId.startsWith('localhost') &&
336
+ portNumber % 10 > 0) {
337
+ // Local testing simulates clusters by running the service on multiple ports.
338
+ // Change the port number to match the cluster ID suffix.
339
+ const clusterNumber = parseInt(clusterId.substring('localhost'.length), 10);
340
+ if (clusterNumber > 0 && clusterNumber < 10) {
341
+ url.port = (portNumber - (portNumber % 10) + clusterNumber).toString();
342
+ baseAddress = url.toString();
343
+ }
344
+ }
345
+ }
346
+ baseAddress = `${baseAddress.replace(/\/$/, '')}${path}`;
347
+ let optionsQuery = this.tunnelRequestOptionsToQueryString(options, query);
348
+ if (optionsQuery) {
349
+ baseAddress += `?${optionsQuery}`;
350
+ }
351
+ return baseAddress;
352
+ }
353
+ buildUriForTunnel(tunnel, path, query, options) {
354
+ let tunnelPath = '';
355
+ if (tunnel.clusterId && tunnel.tunnelId) {
356
+ tunnelPath = `${tunnelsApiPath}/${tunnel.tunnelId}`;
357
+ }
358
+ else {
359
+ if (!tunnel.name) {
360
+ throw new Error('Tunnel object must include either a name or tunnel ID and cluster ID.');
361
+ }
362
+ tunnelPath = `${tunnelsApiPath}/${tunnel.name}`;
363
+ }
364
+ if (options === null || options === void 0 ? void 0 : options.additionalQueryParameters) {
365
+ for (let [paramName, paramValue] of Object.entries(options.additionalQueryParameters)) {
366
+ if (query) {
367
+ query += `&${paramName}=${paramValue}`;
368
+ }
369
+ else {
370
+ query = `${paramName}=${paramValue}`;
371
+ }
372
+ }
373
+ }
374
+ return this.buildUri(tunnel.clusterId, tunnelPath + (path ? path : ''), query, options);
375
+ }
376
+ async getAxiosRequestConfig(tunnel, options, accessTokenScopes) {
377
+ // Get access token header
378
+ let headers = {};
379
+ if (options && options.accessToken) {
380
+ tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.validateTokenExpiration(options.accessToken);
381
+ headers[tunnelAuthentication] = `${tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel} ${options.accessToken}`;
382
+ }
383
+ if (!(tunnelAuthentication in headers) && this.userTokenCallback) {
384
+ let token = await this.userTokenCallback();
385
+ if (token) {
386
+ headers[tunnelAuthentication] = token;
387
+ }
388
+ }
389
+ if (!(tunnelAuthentication in headers)) {
390
+ const accessToken = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.getTunnelAccessToken(tunnel, accessTokenScopes);
391
+ if (accessToken) {
392
+ headers[tunnelAuthentication] = `${tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel} ${accessToken}`;
393
+ }
394
+ }
395
+ const copyAdditionalHeaders = (additionalHeaders) => {
396
+ if (additionalHeaders) {
397
+ for (let [headerName, headerValue] of Object.entries(additionalHeaders)) {
398
+ headers[headerName] = headerValue;
399
+ }
400
+ }
401
+ };
402
+ copyAdditionalHeaders(this.additionalRequestHeaders);
403
+ copyAdditionalHeaders(options === null || options === void 0 ? void 0 : options.additionalHeaders);
404
+ const userAgentPrefix = headers['User-Agent'] ? headers['User-Agent'] + ' ' : '';
405
+ headers['User-Agent'] = `${userAgentPrefix}${this.userAgents} ${version_1.tunnelSdkUserAgent}`;
406
+ // Get axios config
407
+ let config = Object.assign({ headers }, (this.httpsAgent && { httpsAgent: this.httpsAgent }));
408
+ if ((options === null || options === void 0 ? void 0 : options.followRedirects) === false) {
409
+ config.maxRedirects = 0;
410
+ }
411
+ return config;
412
+ }
413
+ convertTunnelForRequest(tunnel) {
414
+ var _a;
415
+ const convertedTunnel = {
416
+ name: tunnel.name,
417
+ domain: tunnel.domain,
418
+ description: tunnel.description,
419
+ tags: tunnel.tags,
420
+ options: tunnel.options,
421
+ accessControl: !tunnel.accessControl
422
+ ? undefined
423
+ : { entries: tunnel.accessControl.entries.filter((ace) => !ace.isInherited) },
424
+ endpoints: tunnel.endpoints,
425
+ ports: (_a = tunnel.ports) === null || _a === void 0 ? void 0 : _a.map((p) => this.convertTunnelPortForRequest(tunnel, p)),
426
+ };
427
+ return convertedTunnel;
428
+ }
429
+ convertTunnelPortForRequest(tunnel, tunnelPort) {
430
+ if (tunnelPort.clusterId && tunnel.clusterId && tunnelPort.clusterId !== tunnel.clusterId) {
431
+ throw new Error('Tunnel port cluster ID does not match tunnel.');
432
+ }
433
+ if (tunnelPort.tunnelId && tunnel.tunnelId && tunnelPort.tunnelId !== tunnel.tunnelId) {
434
+ throw new Error('Tunnel port tunnel ID does not match tunnel.');
435
+ }
436
+ return {
437
+ portNumber: tunnelPort.portNumber,
438
+ protocol: tunnelPort.protocol,
439
+ options: tunnelPort.options,
440
+ accessControl: !tunnelPort.accessControl
441
+ ? undefined
442
+ : { entries: tunnelPort.accessControl.entries.filter((ace) => !ace.isInherited) },
443
+ };
444
+ }
445
+ tunnelRequestOptionsToQueryString(options, additionalQuery) {
446
+ let queryOptions = {};
447
+ const queryItems = [];
448
+ if (options) {
449
+ if (options.includePorts) {
450
+ queryOptions['includePorts'] = ['true'];
451
+ }
452
+ if (options.tokenScopes) {
453
+ dev_tunnels_contracts_1.TunnelAccessControl.validateScopes(options.tokenScopes, undefined, true);
454
+ queryOptions['tokenScopes'] = options.tokenScopes;
455
+ }
456
+ if (options.forceRename) {
457
+ queryOptions['forceRename'] = ['true'];
458
+ }
459
+ if (options.tags) {
460
+ queryOptions['tags'] = options.tags;
461
+ if (options.requireAllTags) {
462
+ queryOptions['allTags'] = ['true'];
463
+ }
464
+ }
465
+ queryItems.push(...Object.keys(queryOptions).map((key) => {
466
+ const value = queryOptions[key];
467
+ return `${key}=${value.map(encodeURIComponent).join(',')}`;
468
+ }));
469
+ }
470
+ if (additionalQuery) {
471
+ queryItems.push(additionalQuery);
472
+ }
473
+ const queryString = queryItems.join('&');
474
+ return queryString;
475
+ }
476
+ /**
477
+ * Makes an HTTP request using Axios, while tracing request and response details.
478
+ */
479
+ async request(method, uri, data, config, allowNotFound) {
480
+ this.trace(`${method} ${uri}`);
481
+ this.traceHeaders(config.headers);
482
+ this.traceContent(data);
483
+ const traceResponse = (response) => {
484
+ this.trace(`${response.status} ${response.statusText}`);
485
+ this.traceHeaders(response.headers);
486
+ this.traceContent(response.data);
487
+ };
488
+ try {
489
+ config.url = uri;
490
+ config.method = method;
491
+ config.data = data;
492
+ let response = await axios_1.default.request(config);
493
+ traceResponse(response);
494
+ // This assumes that TResult is always boolean for DELETE requests.
495
+ return (method === 'DELETE' ? true : response.data);
496
+ }
497
+ catch (e) {
498
+ if (!(e instanceof Error) || !e.isAxiosError)
499
+ throw e;
500
+ const requestError = e;
501
+ if (requestError.response) {
502
+ traceResponse(requestError.response);
503
+ if (allowNotFound && requestError.response.status === 404) {
504
+ return (method === 'DELETE' ? false : null);
505
+ }
506
+ }
507
+ requestError.message = this.getResponseErrorMessage(requestError);
508
+ // Axios errors have too much redundant detail! Delete some of it.
509
+ delete requestError.request;
510
+ if (requestError.response) {
511
+ delete requestError.config.httpAgent;
512
+ delete requestError.config.httpsAgent;
513
+ delete requestError.response.request;
514
+ }
515
+ throw requestError;
516
+ }
517
+ }
518
+ traceHeaders(headers) {
519
+ for (let [headerName, headerValue] of Object.entries(headers)) {
520
+ if (headerName === 'Authorization') {
521
+ this.traceAuthorizationHeader(headerName, headerValue);
522
+ return;
523
+ }
524
+ this.trace(`${headerName}: ${headerValue !== null && headerValue !== void 0 ? headerValue : ''}`);
525
+ }
526
+ }
527
+ traceAuthorizationHeader(key, value) {
528
+ if (typeof value !== 'string')
529
+ return;
530
+ const spaceIndex = value.indexOf(' ');
531
+ if (spaceIndex < 0) {
532
+ this.trace(`${key}: [${value.length}]`);
533
+ return;
534
+ }
535
+ const scheme = value.substring(0, spaceIndex);
536
+ const token = value.substring(spaceIndex + 1);
537
+ if (scheme.toLowerCase() === tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel.toLowerCase()) {
538
+ const tokenProperties = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.tryParse(token);
539
+ if (tokenProperties) {
540
+ this.trace(`${key}: ${scheme} <${tokenProperties}>`);
541
+ return;
542
+ }
543
+ }
544
+ this.trace(`${key}: ${scheme} <token>`);
545
+ }
546
+ traceContent(data) {
547
+ if (typeof data === 'object') {
548
+ data = JSON.stringify(data, undefined, ' ');
549
+ }
550
+ if (typeof data === 'string') {
551
+ this.trace(TunnelManagementHttpClient.replaceTokensInContent(data));
552
+ }
553
+ }
554
+ static replaceTokensInContent(content) {
555
+ var _a;
556
+ const tokenRegex = /"(eyJ[a-zA-z0-9\-_]+\.[a-zA-z0-9\-_]+\.[a-zA-z0-9\-_]+)"/;
557
+ let match = tokenRegex.exec(content);
558
+ while (match) {
559
+ let token = match[1];
560
+ const tokenProperties = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.tryParse(token);
561
+ token = (_a = tokenProperties === null || tokenProperties === void 0 ? void 0 : tokenProperties.toString()) !== null && _a !== void 0 ? _a : 'token';
562
+ content =
563
+ content.substring(0, match.index + 1) +
564
+ '<' +
565
+ token +
566
+ '>' +
567
+ content.substring(match.index + match[0].length - 1);
568
+ match = tokenRegex.exec(content);
569
+ }
570
+ return content;
571
+ }
572
+ }
573
+ exports.TunnelManagementHttpClient = TunnelManagementHttpClient;
574
574
  //# sourceMappingURL=tunnelManagementHttpClient.js.map