787 lines
38 KiB
JavaScript
787 lines
38 KiB
JavaScript
"use strict";
|
|
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.TunnelManagementHttpClient = exports.ManagementApiVersions = void 0;
|
|
const dev_tunnels_contracts_1 = require("@microsoft/dev-tunnels-contracts");
|
|
const tunnelManagementClient_1 = require("./tunnelManagementClient");
|
|
const tunnelAccessTokenProperties_1 = require("./tunnelAccessTokenProperties");
|
|
const version_1 = require("./version");
|
|
const axios_1 = require("axios");
|
|
const tunnelPlanTokenProperties_1 = require("./tunnelPlanTokenProperties");
|
|
const idGeneration_1 = require("./idGeneration");
|
|
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
|
|
const tunnelsApiPath = '/tunnels';
|
|
const limitsApiPath = '/userlimits';
|
|
const endpointsApiSubPath = '/endpoints';
|
|
const portsApiSubPath = '/ports';
|
|
const clustersApiPath = '/clusters';
|
|
const tunnelAuthentication = 'Authorization';
|
|
const checkAvailablePath = ':checkNameAvailability';
|
|
const createNameRetries = 3;
|
|
var ManagementApiVersions;
|
|
(function (ManagementApiVersions) {
|
|
ManagementApiVersions["Version20230927preview"] = "2023-09-27-preview";
|
|
})(ManagementApiVersions = exports.ManagementApiVersions || (exports.ManagementApiVersions = {}));
|
|
function comparePorts(a, b) {
|
|
var _a, _b;
|
|
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);
|
|
}
|
|
function parseDate(value) {
|
|
return typeof value === 'string' ? new Date(Date.parse(value)) : value;
|
|
}
|
|
/**
|
|
* Fixes Tunnel properties of type Date that were deserialized as strings.
|
|
*/
|
|
function parseTunnelDates(tunnel) {
|
|
if (!tunnel)
|
|
return;
|
|
tunnel.created = parseDate(tunnel.created);
|
|
if (tunnel.status) {
|
|
tunnel.status.lastHostConnectionTime = parseDate(tunnel.status.lastHostConnectionTime);
|
|
tunnel.status.lastClientConnectionTime = parseDate(tunnel.status.lastClientConnectionTime);
|
|
}
|
|
}
|
|
/**
|
|
* Fixes TunnelPort properties of type Date that were deserialized as strings.
|
|
*/
|
|
function parseTunnelPortDates(port) {
|
|
if (!port)
|
|
return;
|
|
if (port.status) {
|
|
port.status.lastClientConnectionTime = parseDate(port.status.lastClientConnectionTime);
|
|
}
|
|
}
|
|
/**
|
|
* Copy access tokens from the request object to the result object, except for any
|
|
* tokens that were refreshed by the request.
|
|
*/
|
|
function preserveAccessTokens(requestObject, resultObject) {
|
|
var _a;
|
|
// This intentionally does not check whether any existing tokens are expired. So
|
|
// expired tokens may be preserved also, if not refreshed. This allows for better
|
|
// diagnostics in that case.
|
|
if (requestObject.accessTokens && resultObject) {
|
|
(_a = resultObject.accessTokens) !== null && _a !== void 0 ? _a : (resultObject.accessTokens = {});
|
|
for (const scopeAndToken of Object.entries(requestObject.accessTokens)) {
|
|
if (!resultObject.accessTokens[scopeAndToken[0]]) {
|
|
resultObject.accessTokens[scopeAndToken[0]] = scopeAndToken[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const manageAccessTokenScope = [dev_tunnels_contracts_1.TunnelAccessScopes.Manage];
|
|
const hostAccessTokenScope = [dev_tunnels_contracts_1.TunnelAccessScopes.Host];
|
|
const managePortsAccessTokenScopes = [
|
|
dev_tunnels_contracts_1.TunnelAccessScopes.Manage,
|
|
dev_tunnels_contracts_1.TunnelAccessScopes.ManagePorts,
|
|
dev_tunnels_contracts_1.TunnelAccessScopes.Host,
|
|
];
|
|
const readAccessTokenScopes = [
|
|
dev_tunnels_contracts_1.TunnelAccessScopes.Manage,
|
|
dev_tunnels_contracts_1.TunnelAccessScopes.ManagePorts,
|
|
dev_tunnels_contracts_1.TunnelAccessScopes.Host,
|
|
dev_tunnels_contracts_1.TunnelAccessScopes.Connect,
|
|
];
|
|
const apiVersions = ["2023-09-27-preview"];
|
|
const defaultRequestTimeoutMS = 20000;
|
|
class TunnelManagementHttpClient {
|
|
/**
|
|
* Initializes a new instance of the `TunnelManagementHttpClient` class
|
|
* with a client authentication callback, service URI, and HTTP handler.
|
|
*
|
|
* @param userAgent { name, version } object or a comment string to use as the User-Agent header.
|
|
* @param apiVersion ApiVersion to be used for requests, value should be one of ManagementApiVersions enum.
|
|
* @param userTokenCallback Optional async callback for retrieving a client authentication
|
|
* header value with access token, for AAD or GitHub user authentication. This may be omitted
|
|
* for anonymous tunnel clients, or if tunnel access tokens will be specified via
|
|
* `TunnelRequestOptions.accessToken`.
|
|
* @param tunnelServiceUri Optional tunnel service URI (not including any path). Defaults to
|
|
* the global tunnel service URI.
|
|
* @param httpsAgent Optional agent that will be invoked for HTTPS requests to the tunnel
|
|
* service.
|
|
* @param adapter Optional axios adapter to use for HTTP requests.
|
|
*/
|
|
constructor(userAgents, apiVersion, userTokenCallback, tunnelServiceUri, httpsAgent, adapter) {
|
|
var _a;
|
|
this.httpsAgent = httpsAgent;
|
|
this.adapter = adapter;
|
|
this.reportProgressEmitter = new vscode_jsonrpc_1.Emitter();
|
|
/**
|
|
* Event that is raised to report tunnel management progress.
|
|
*
|
|
* See `Progress` for a description of the different progress events that can be reported.
|
|
*/
|
|
this.onReportProgress = this.reportProgressEmitter.event;
|
|
this.trace = (msg) => { };
|
|
if (apiVersions.indexOf(apiVersion) === -1) {
|
|
throw new TypeError(`Invalid API version: ${apiVersion}, must be one of ${apiVersions}`);
|
|
}
|
|
this.apiVersion = apiVersion;
|
|
if (!userAgents) {
|
|
throw new TypeError('User agent must be provided.');
|
|
}
|
|
if (Array.isArray(userAgents)) {
|
|
if (userAgents.length === 0) {
|
|
throw new TypeError('User agents cannot be empty.');
|
|
}
|
|
let combinedUserAgents = '';
|
|
userAgents.forEach((userAgent) => {
|
|
var _a;
|
|
if (typeof userAgent !== 'string') {
|
|
if (!userAgent.name) {
|
|
throw new TypeError('Invalid user agent. The name must be provided.');
|
|
}
|
|
if (typeof userAgent.name !== 'string') {
|
|
throw new TypeError('Invalid user agent. The name must be a string.');
|
|
}
|
|
if (userAgent.version && typeof userAgent.version !== 'string') {
|
|
throw new TypeError('Invalid user agent. The version must be a string.');
|
|
}
|
|
combinedUserAgents = `${combinedUserAgents}${userAgent.name}/${(_a = userAgent.version) !== null && _a !== void 0 ? _a : 'unknown'} `;
|
|
}
|
|
else {
|
|
combinedUserAgents = `${combinedUserAgents}${userAgent} `;
|
|
}
|
|
});
|
|
this.userAgents = combinedUserAgents.trim();
|
|
}
|
|
else if (typeof userAgents !== 'string') {
|
|
if (!userAgents.name) {
|
|
throw new TypeError('Invalid user agent. The name must be provided.');
|
|
}
|
|
if (typeof userAgents.name !== 'string') {
|
|
throw new TypeError('Invalid user agent. The name must be a string.');
|
|
}
|
|
if (userAgents.version && typeof userAgents.version !== 'string') {
|
|
throw new TypeError('Invalid user agent. The version must be a string.');
|
|
}
|
|
this.userAgents = `${userAgents.name}/${(_a = userAgents.version) !== null && _a !== void 0 ? _a : 'unknown'}`;
|
|
}
|
|
else {
|
|
this.userAgents = userAgents;
|
|
}
|
|
this.userTokenCallback = userTokenCallback !== null && userTokenCallback !== void 0 ? userTokenCallback : (() => Promise.resolve(null));
|
|
if (!tunnelServiceUri) {
|
|
tunnelServiceUri = dev_tunnels_contracts_1.TunnelServiceProperties.production.serviceUri;
|
|
}
|
|
const parsedUri = new URL(tunnelServiceUri);
|
|
if (!parsedUri || parsedUri.pathname !== '/') {
|
|
throw new TypeError(`Invalid tunnel service URI: ${tunnelServiceUri}`);
|
|
}
|
|
this.baseAddress = tunnelServiceUri;
|
|
}
|
|
async listTunnels(clusterId, domain, options, cancellation) {
|
|
const queryParams = [clusterId ? null : 'global=true', domain ? `domain=${domain}` : null];
|
|
const query = queryParams.filter((p) => !!p).join('&');
|
|
const results = (await this.sendRequest('GET', clusterId, tunnelsApiPath, query, options, undefined, undefined, cancellation));
|
|
let tunnels = new Array();
|
|
if (results.value) {
|
|
for (const region of results.value) {
|
|
if (region.value) {
|
|
tunnels = tunnels.concat(region.value);
|
|
}
|
|
}
|
|
}
|
|
tunnels.forEach(parseTunnelDates);
|
|
return tunnels;
|
|
}
|
|
async getTunnel(tunnel, options, cancellation) {
|
|
const result = await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, undefined, undefined, options, undefined, undefined, cancellation);
|
|
preserveAccessTokens(tunnel, result);
|
|
parseTunnelDates(result);
|
|
return result;
|
|
}
|
|
async createTunnel(tunnel, options, cancellation) {
|
|
const tunnelId = tunnel.tunnelId;
|
|
const idGenerated = tunnelId === undefined || tunnelId === null || tunnelId === '';
|
|
options = options || {};
|
|
options.additionalHeaders = options.additionalHeaders || {};
|
|
options.additionalHeaders['If-Not-Match'] = "*";
|
|
if (idGenerated) {
|
|
tunnel.tunnelId = idGeneration_1.IdGeneration.generateTunnelId();
|
|
}
|
|
for (let i = 0; i <= createNameRetries; i++) {
|
|
try {
|
|
const result = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel), undefined, cancellation, true));
|
|
preserveAccessTokens(tunnel, result);
|
|
parseTunnelDates(result);
|
|
return result;
|
|
}
|
|
catch (error) {
|
|
if (idGenerated) {
|
|
// The tunnel ID was generated and there was a conflict.
|
|
// Try again with a new ID.
|
|
tunnel.tunnelId = idGeneration_1.IdGeneration.generateTunnelId();
|
|
}
|
|
else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
const result2 = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel), undefined, cancellation, true));
|
|
preserveAccessTokens(tunnel, result2);
|
|
parseTunnelDates(result2);
|
|
return result2;
|
|
}
|
|
async createOrUpdateTunnel(tunnel, options, cancellation) {
|
|
const tunnelId = tunnel.tunnelId;
|
|
const idGenerated = tunnelId === undefined || tunnelId === null || tunnelId === '';
|
|
if (idGenerated) {
|
|
tunnel.tunnelId = idGeneration_1.IdGeneration.generateTunnelId();
|
|
}
|
|
for (let i = 0; i <= createNameRetries; i++) {
|
|
try {
|
|
const result = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel), undefined, cancellation, true));
|
|
preserveAccessTokens(tunnel, result);
|
|
parseTunnelDates(result);
|
|
return result;
|
|
}
|
|
catch (error) {
|
|
if (idGenerated) {
|
|
// The tunnel ID was generated and there was a conflict.
|
|
// Try again with a new ID.
|
|
tunnel.tunnelId = idGeneration_1.IdGeneration.generateTunnelId();
|
|
}
|
|
else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
const result2 = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, "forceCreate=true", options, this.convertTunnelForRequest(tunnel), undefined, cancellation, true));
|
|
preserveAccessTokens(tunnel, result2);
|
|
parseTunnelDates(result2);
|
|
return result2;
|
|
}
|
|
async updateTunnel(tunnel, options, cancellation) {
|
|
options = options || {};
|
|
options.additionalHeaders = options.additionalHeaders || {};
|
|
options.additionalHeaders['If-Match'] = "*";
|
|
const result = (await this.sendTunnelRequest('PUT', tunnel, manageAccessTokenScope, undefined, undefined, options, this.convertTunnelForRequest(tunnel), undefined, cancellation));
|
|
preserveAccessTokens(tunnel, result);
|
|
parseTunnelDates(result);
|
|
return result;
|
|
}
|
|
async deleteTunnel(tunnel, options, cancellation) {
|
|
return await this.sendTunnelRequest('DELETE', tunnel, manageAccessTokenScope, undefined, undefined, options, undefined, true, cancellation);
|
|
}
|
|
async updateTunnelEndpoint(tunnel, endpoint, options, cancellation) {
|
|
if (endpoint.id == null) {
|
|
throw new Error('Endpoint ID must be specified when updating an endpoint.');
|
|
}
|
|
const path = `${endpointsApiSubPath}/${endpoint.id}`;
|
|
const result = (await this.sendTunnelRequest('PUT', tunnel, hostAccessTokenScope, path, "connectionMode=" + endpoint.connectionMode, options, endpoint, undefined, cancellation));
|
|
if (tunnel.endpoints) {
|
|
// Also update the endpoint in the local tunnel object.
|
|
tunnel.endpoints = tunnel.endpoints
|
|
.filter((e) => e.hostId !== endpoint.hostId ||
|
|
e.connectionMode !== endpoint.connectionMode)
|
|
.concat(result);
|
|
}
|
|
return result;
|
|
}
|
|
async deleteTunnelEndpoints(tunnel, id, options, cancellation) {
|
|
const path = `${endpointsApiSubPath}/${id}`;
|
|
const result = await this.sendTunnelRequest('DELETE', tunnel, hostAccessTokenScope, path, undefined, options, undefined, true, cancellation);
|
|
if (result && tunnel.endpoints) {
|
|
// Also delete the endpoint in the local tunnel object.
|
|
tunnel.endpoints = tunnel.endpoints.filter((e) => e.id !== id);
|
|
}
|
|
return result;
|
|
}
|
|
async listUserLimits(cancellation) {
|
|
const results = await this.sendRequest('GET', undefined, limitsApiPath, undefined, undefined, undefined, undefined, cancellation);
|
|
return results || [];
|
|
}
|
|
async listTunnelPorts(tunnel, options, cancellation) {
|
|
const results = (await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, portsApiSubPath, undefined, options, undefined, undefined, cancellation));
|
|
if (results.value) {
|
|
results.value.forEach(parseTunnelPortDates);
|
|
}
|
|
return results.value;
|
|
}
|
|
async getTunnelPort(tunnel, portNumber, options, cancellation) {
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingGetTunnelPort);
|
|
const path = `${portsApiSubPath}/${portNumber}`;
|
|
const result = await this.sendTunnelRequest('GET', tunnel, readAccessTokenScopes, path, undefined, options, undefined, undefined, cancellation);
|
|
parseTunnelPortDates(result);
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedGetTunnelPort);
|
|
return result;
|
|
}
|
|
async createTunnelPort(tunnel, tunnelPort, options, cancellation) {
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingCreateTunnelPort);
|
|
tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
|
|
const path = `${portsApiSubPath}/${tunnelPort.portNumber}`;
|
|
options = options || {};
|
|
options.additionalHeaders = options.additionalHeaders || {};
|
|
options.additionalHeaders['If-Not-Match'] = "*";
|
|
const result = (await this.sendTunnelRequest('PUT', tunnel, managePortsAccessTokenScopes, path, undefined, options, tunnelPort, undefined, cancellation));
|
|
tunnel.ports = tunnel.ports || [];
|
|
// Also add the port to the local tunnel object.
|
|
tunnel.ports = tunnel.ports
|
|
.filter((p) => p.portNumber !== tunnelPort.portNumber)
|
|
.concat(result)
|
|
.sort(comparePorts);
|
|
parseTunnelPortDates(result);
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedCreateTunnelPort);
|
|
return result;
|
|
}
|
|
async updateTunnelPort(tunnel, tunnelPort, options, cancellation) {
|
|
if (tunnelPort.clusterId && tunnel.clusterId && tunnelPort.clusterId !== tunnel.clusterId) {
|
|
throw new Error('Tunnel port cluster ID is not consistent.');
|
|
}
|
|
options = options || {};
|
|
options.additionalHeaders = options.additionalHeaders || {};
|
|
options.additionalHeaders['If-Match'] = "*";
|
|
const portNumber = tunnelPort.portNumber;
|
|
const path = `${portsApiSubPath}/${portNumber}`;
|
|
tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
|
|
const result = (await this.sendTunnelRequest('PUT', tunnel, managePortsAccessTokenScopes, path, undefined, options, tunnelPort, undefined, cancellation));
|
|
preserveAccessTokens(tunnelPort, result);
|
|
parseTunnelPortDates(result);
|
|
tunnel.ports = tunnel.ports || [];
|
|
// Also add the port to the local tunnel object.
|
|
tunnel.ports = tunnel.ports
|
|
.filter((p) => p.portNumber !== tunnelPort.portNumber)
|
|
.concat(result)
|
|
.sort(comparePorts);
|
|
return result;
|
|
}
|
|
async createOrUpdateTunnelPort(tunnel, tunnelPort, options, cancellation) {
|
|
tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort);
|
|
const path = `${portsApiSubPath}/${tunnelPort.portNumber}`;
|
|
const result = (await this.sendTunnelRequest('PUT', tunnel, managePortsAccessTokenScopes, path, undefined, options, tunnelPort, undefined, cancellation));
|
|
tunnel.ports = tunnel.ports || [];
|
|
// Also add the port to the local tunnel object.
|
|
tunnel.ports = tunnel.ports
|
|
.filter((p) => p.portNumber !== tunnelPort.portNumber)
|
|
.concat(result)
|
|
.sort(comparePorts);
|
|
parseTunnelPortDates(result);
|
|
return result;
|
|
}
|
|
async deleteTunnelPort(tunnel, portNumber, options, cancellation) {
|
|
const path = `${portsApiSubPath}/${portNumber}`;
|
|
const result = await this.sendTunnelRequest('DELETE', tunnel, managePortsAccessTokenScopes, path, undefined, options, undefined, true, cancellation);
|
|
if (result && tunnel.ports) {
|
|
// Also delete the port in the local tunnel object.
|
|
tunnel.ports = tunnel.ports
|
|
.filter((p) => p.portNumber !== portNumber)
|
|
.sort(comparePorts);
|
|
}
|
|
return result;
|
|
}
|
|
async listClusters(cancellation) {
|
|
return (await this.sendRequest('GET', undefined, clustersApiPath, undefined, undefined, undefined, false, cancellation));
|
|
}
|
|
/**
|
|
* Sends an HTTP request to the tunnel management API, targeting a specific tunnel.
|
|
* This protected method enables subclasses to support additional tunnel management APIs.
|
|
* @param method HTTP request method.
|
|
* @param tunnel Tunnel that the request is targeting.
|
|
* @param accessTokenScopes Required array of access scopes for tokens in `tunnel.accessTokens`
|
|
* that could be used to authorize the request.
|
|
* @param path Optional request sub-path relative to the tunnel.
|
|
* @param query Optional query string to append to the request.
|
|
* @param options Request options.
|
|
* @param body Optional request body object.
|
|
* @param allowNotFound If true, a 404 response is returned as a null or false result
|
|
* instead of an error.
|
|
* @param cancellationToken Optional cancellation token for the request.
|
|
* @param isCreate Set to true if this is a tunnel create request, default is false.
|
|
* @returns Result of the request.
|
|
*/
|
|
async sendTunnelRequest(method, tunnel, accessTokenScopes, path, query, options, body, allowNotFound, cancellation, isCreate = false) {
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingRequestUri);
|
|
const uri = await this.buildUriForTunnel(tunnel, path, query, options, isCreate);
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingRequestConfig);
|
|
const config = await this.getAxiosRequestConfig(tunnel, options, accessTokenScopes);
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingSendTunnelRequest);
|
|
const result = await this.request(method, uri, body, config, allowNotFound, cancellation);
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedSendTunnelRequest);
|
|
return result;
|
|
}
|
|
/**
|
|
* Sends an HTTP request to the tunnel management API.
|
|
* This protected method enables subclasses to support additional tunnel management APIs.
|
|
* @param method HTTP request method.
|
|
* @param clusterId Optional tunnel service cluster ID to direct the request to. If unspecified,
|
|
* the request will use the global traffic-manager to find the nearest cluster.
|
|
* @param path Required request path.
|
|
* @param query Optional query string to append to the request.
|
|
* @param options Request options.
|
|
* @param body Optional request body object.
|
|
* @param allowNotFound If true, a 404 response is returned as a null or false result
|
|
* instead of an error.
|
|
* @param cancellationToken Optional cancellation token for the request.
|
|
* @returns Result of the request.
|
|
*/
|
|
async sendRequest(method, clusterId, path, query, options, body, allowNotFound, cancellation) {
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingSendTunnelRequest);
|
|
const uri = await this.buildUri(clusterId, path, query, options);
|
|
const config = await this.getAxiosRequestConfig(undefined, options);
|
|
const result = await this.request(method, uri, body, config, allowNotFound, cancellation);
|
|
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedSendTunnelRequest);
|
|
return result;
|
|
}
|
|
async checkNameAvailablility(tunnelName, cancellation) {
|
|
tunnelName = encodeURI(tunnelName);
|
|
const uri = await this.buildUri(undefined, `${tunnelsApiPath}/${tunnelName}${checkAvailablePath}`);
|
|
const config = {
|
|
httpsAgent: this.httpsAgent,
|
|
adapter: this.adapter,
|
|
};
|
|
return await this.request('GET', uri, undefined, config, undefined, cancellation);
|
|
}
|
|
raiseReportProgress(progress) {
|
|
const args = {
|
|
progress: progress
|
|
};
|
|
this.reportProgressEmitter.fire(args);
|
|
}
|
|
getResponseErrorMessage(error, signal) {
|
|
var _a, _b, _c, _d;
|
|
let errorMessage = '';
|
|
if (error.code === 'ECONNABORTED') {
|
|
// server timeout
|
|
errorMessage = `Timeout reached: ${error.message}`;
|
|
}
|
|
if (signal.aborted) {
|
|
// connection timeout
|
|
errorMessage = `Signal aborted: ${error.message}`;
|
|
}
|
|
if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
|
|
const problemDetails = error.response.data;
|
|
if (problemDetails.title || problemDetails.detail) {
|
|
errorMessage = `Tunnel service error: ${problemDetails.title}`;
|
|
if (problemDetails.detail) {
|
|
errorMessage += ' ' + problemDetails.detail;
|
|
}
|
|
if (problemDetails.errors) {
|
|
errorMessage += JSON.stringify(problemDetails.errors);
|
|
}
|
|
}
|
|
}
|
|
if (!errorMessage) {
|
|
if (error === null || error === void 0 ? void 0 : error.response) {
|
|
errorMessage =
|
|
'Tunnel service returned status code: ' +
|
|
`${error.response.status} ${error.response.statusText}`;
|
|
}
|
|
else {
|
|
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.';
|
|
}
|
|
}
|
|
const requestIdHeaderName = 'VsSaaS-Request-Id';
|
|
if (((_d = error.response) === null || _d === void 0 ? void 0 : _d.headers) && error.response.headers[requestIdHeaderName]) {
|
|
errorMessage += `\nRequest ID: ${error.response.headers[requestIdHeaderName]}`;
|
|
}
|
|
return errorMessage;
|
|
}
|
|
// Helper functions
|
|
async buildUri(clusterId, path, query, options) {
|
|
if (clusterId === undefined && this.userTokenCallback) {
|
|
let token = await this.userTokenCallback();
|
|
if (token && token.startsWith("tunnelplan")) {
|
|
token = token.replace("tunnelplan ", "");
|
|
const parsedToken = tunnelPlanTokenProperties_1.TunnelPlanTokenProperties.tryParse(token);
|
|
if (parsedToken !== null && parsedToken.clusterId) {
|
|
clusterId = parsedToken.clusterId;
|
|
}
|
|
}
|
|
}
|
|
let baseAddress = this.baseAddress;
|
|
if (clusterId) {
|
|
const url = new URL(baseAddress);
|
|
const portNumber = parseInt(url.port, 10);
|
|
if (url.hostname !== 'localhost' && !url.hostname.startsWith(`${clusterId}.`)) {
|
|
// A specific cluster ID was specified (while not running on localhost).
|
|
// Prepend the cluster ID to the hostname, and optionally strip a global prefix.
|
|
url.hostname = `${clusterId}.${url.hostname}`.replace('global.', '');
|
|
baseAddress = url.toString();
|
|
}
|
|
else if (url.protocol === 'https:' &&
|
|
clusterId.startsWith('localhost') &&
|
|
portNumber % 10 > 0) {
|
|
// Local testing simulates clusters by running the service on multiple ports.
|
|
// Change the port number to match the cluster ID suffix.
|
|
const clusterNumber = parseInt(clusterId.substring('localhost'.length), 10);
|
|
if (clusterNumber > 0 && clusterNumber < 10) {
|
|
url.port = (portNumber - (portNumber % 10) + clusterNumber).toString();
|
|
baseAddress = url.toString();
|
|
}
|
|
}
|
|
}
|
|
baseAddress = `${baseAddress.replace(/\/$/, '')}${path}`;
|
|
const optionsQuery = this.tunnelRequestOptionsToQueryString(options, query);
|
|
if (optionsQuery) {
|
|
baseAddress += `?${optionsQuery}`;
|
|
}
|
|
return baseAddress;
|
|
}
|
|
buildUriForTunnel(tunnel, path, query, options, isCreate = false) {
|
|
let tunnelPath = '';
|
|
if ((tunnel.clusterId || isCreate) && tunnel.tunnelId) {
|
|
tunnelPath = `${tunnelsApiPath}/${tunnel.tunnelId}`;
|
|
}
|
|
else {
|
|
throw new Error('Tunnel object must include a tunnel ID always and cluster ID for non creates.');
|
|
}
|
|
if (options === null || options === void 0 ? void 0 : options.additionalQueryParameters) {
|
|
for (const [paramName, paramValue] of Object.entries(options.additionalQueryParameters)) {
|
|
if (query) {
|
|
query += `&${paramName}=${paramValue}`;
|
|
}
|
|
else {
|
|
query = `${paramName}=${paramValue}`;
|
|
}
|
|
}
|
|
}
|
|
return this.buildUri(tunnel.clusterId, tunnelPath + (path ? path : ''), query, options);
|
|
}
|
|
async getAxiosRequestConfig(tunnel, options, accessTokenScopes) {
|
|
// Get access token header
|
|
const headers = {};
|
|
if (options && options.accessToken) {
|
|
tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.validateTokenExpiration(options.accessToken);
|
|
headers[tunnelAuthentication] = `${tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel} ${options.accessToken}`;
|
|
}
|
|
if (!(tunnelAuthentication in headers) && this.userTokenCallback) {
|
|
const token = await this.userTokenCallback();
|
|
if (token) {
|
|
headers[tunnelAuthentication] = token;
|
|
}
|
|
}
|
|
if (!(tunnelAuthentication in headers)) {
|
|
const accessToken = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.getTunnelAccessToken(tunnel, accessTokenScopes);
|
|
if (accessToken) {
|
|
headers[tunnelAuthentication] = `${tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel} ${accessToken}`;
|
|
}
|
|
}
|
|
const copyAdditionalHeaders = (additionalHeaders) => {
|
|
if (additionalHeaders) {
|
|
for (const [headerName, headerValue] of Object.entries(additionalHeaders)) {
|
|
headers[headerName] = headerValue;
|
|
}
|
|
}
|
|
};
|
|
copyAdditionalHeaders(this.additionalRequestHeaders);
|
|
copyAdditionalHeaders(options === null || options === void 0 ? void 0 : options.additionalHeaders);
|
|
const userAgentPrefix = headers['User-Agent'] ? headers['User-Agent'] + ' ' : '';
|
|
headers['User-Agent'] = `${userAgentPrefix}${this.userAgents} ${version_1.tunnelSdkUserAgent}`;
|
|
// Get axios config
|
|
const config = Object.assign(Object.assign({ headers }, (this.httpsAgent && { httpsAgent: this.httpsAgent })), (this.adapter && { adapter: this.adapter }));
|
|
if ((options === null || options === void 0 ? void 0 : options.followRedirects) === false) {
|
|
config.maxRedirects = 0;
|
|
}
|
|
return config;
|
|
}
|
|
convertTunnelForRequest(tunnel) {
|
|
var _a;
|
|
const convertedTunnel = {
|
|
tunnelId: tunnel.tunnelId,
|
|
name: tunnel.name,
|
|
domain: tunnel.domain,
|
|
description: tunnel.description,
|
|
labels: tunnel.labels,
|
|
options: tunnel.options,
|
|
customExpiration: tunnel.customExpiration,
|
|
accessControl: !tunnel.accessControl
|
|
? undefined
|
|
: { entries: tunnel.accessControl.entries.filter((ace) => !ace.isInherited) },
|
|
endpoints: tunnel.endpoints,
|
|
ports: (_a = tunnel.ports) === null || _a === void 0 ? void 0 : _a.map((p) => this.convertTunnelPortForRequest(tunnel, p)),
|
|
};
|
|
return convertedTunnel;
|
|
}
|
|
convertTunnelPortForRequest(tunnel, tunnelPort) {
|
|
if (tunnelPort.clusterId && tunnel.clusterId && tunnelPort.clusterId !== tunnel.clusterId) {
|
|
throw new Error('Tunnel port cluster ID does not match tunnel.');
|
|
}
|
|
if (tunnelPort.tunnelId && tunnel.tunnelId && tunnelPort.tunnelId !== tunnel.tunnelId) {
|
|
throw new Error('Tunnel port tunnel ID does not match tunnel.');
|
|
}
|
|
return {
|
|
portNumber: tunnelPort.portNumber,
|
|
protocol: tunnelPort.protocol,
|
|
isDefault: tunnelPort.isDefault,
|
|
description: tunnelPort.description,
|
|
labels: tunnelPort.labels,
|
|
sshUser: tunnelPort.sshUser,
|
|
options: tunnelPort.options,
|
|
accessControl: !tunnelPort.accessControl
|
|
? undefined
|
|
: { entries: tunnelPort.accessControl.entries.filter((ace) => !ace.isInherited) },
|
|
};
|
|
}
|
|
tunnelRequestOptionsToQueryString(options, additionalQuery) {
|
|
const queryOptions = {};
|
|
const queryItems = [];
|
|
if (options) {
|
|
if (options.includePorts) {
|
|
queryOptions.includePorts = ['true'];
|
|
}
|
|
if (options.includeAccessControl) {
|
|
queryOptions.includeAccessControl = ['true'];
|
|
}
|
|
if (options.tokenScopes) {
|
|
dev_tunnels_contracts_1.TunnelAccessControl.validateScopes(options.tokenScopes, undefined, true);
|
|
queryOptions.tokenScopes = options.tokenScopes;
|
|
}
|
|
if (options.forceRename) {
|
|
queryOptions.forceRename = ['true'];
|
|
}
|
|
if (options.labels) {
|
|
queryOptions.labels = options.labels;
|
|
if (options.requireAllLabels) {
|
|
queryOptions.allLabels = ['true'];
|
|
}
|
|
}
|
|
if (options.limit) {
|
|
queryOptions.limit = [options.limit.toString()];
|
|
}
|
|
queryItems.push(...Object.keys(queryOptions).map((key) => {
|
|
const value = queryOptions[key];
|
|
return `${key}=${value.map(encodeURIComponent).join(',')}`;
|
|
}));
|
|
}
|
|
if (additionalQuery) {
|
|
queryItems.push(additionalQuery);
|
|
}
|
|
queryItems.push(`api-version=${this.apiVersion}`);
|
|
const queryString = queryItems.join('&');
|
|
return queryString;
|
|
}
|
|
/**
|
|
* Axios request that can be overridden for unit tests purposes.
|
|
* @param config axios request config
|
|
* @param _cancellation the cancellation token for the request (used by unit tests to simulate timeouts).
|
|
*/
|
|
async axiosRequest(config, _cancellation) {
|
|
return await axios_1.default.request(config);
|
|
}
|
|
/**
|
|
* Makes an HTTP request using Axios, while tracing request and response details.
|
|
*/
|
|
async request(method, uri, data, config, allowNotFound, cancellation) {
|
|
var _a, _b;
|
|
this.trace(`${method} ${uri}`);
|
|
if (config.headers) {
|
|
this.traceHeaders(config.headers);
|
|
}
|
|
this.traceContent(data);
|
|
const traceResponse = (response) => {
|
|
this.trace(`${response.status} ${response.statusText}`);
|
|
this.traceHeaders(response.headers);
|
|
this.traceContent(response.data);
|
|
};
|
|
let disposable;
|
|
const abortController = new AbortController();
|
|
let timeout = undefined;
|
|
const newAbortSignal = () => {
|
|
if (cancellation === null || cancellation === void 0 ? void 0 : cancellation.isCancellationRequested) {
|
|
abortController.abort('Cancelled: CancellationToken cancel requested.');
|
|
}
|
|
else if (cancellation) {
|
|
disposable = cancellation.onCancellationRequested(() => abortController.abort('Cancelled: CancellationToken cancel requested.'));
|
|
}
|
|
else {
|
|
timeout = setTimeout(() => abortController.abort('Cancelled: default request timeout reached.'), defaultRequestTimeoutMS);
|
|
}
|
|
return abortController.signal;
|
|
};
|
|
try {
|
|
config.url = uri;
|
|
config.method = method;
|
|
config.data = data;
|
|
config.signal = newAbortSignal();
|
|
config.timeout = defaultRequestTimeoutMS;
|
|
const response = await this.axiosRequest(config, cancellation);
|
|
traceResponse(response);
|
|
// This assumes that TResult is always boolean for DELETE requests.
|
|
return (method === 'DELETE' ? true : response.data);
|
|
}
|
|
catch (e) {
|
|
if (!(e instanceof Error) || !e.isAxiosError)
|
|
throw e;
|
|
const requestError = e;
|
|
if (requestError.response) {
|
|
traceResponse(requestError.response);
|
|
if (allowNotFound && requestError.response.status === 404) {
|
|
return (method === 'DELETE' ? false : null);
|
|
}
|
|
}
|
|
requestError.message = this.getResponseErrorMessage(requestError, abortController.signal);
|
|
// Axios errors have too much redundant detail! Delete some of it.
|
|
delete requestError.request;
|
|
if (requestError.response) {
|
|
(_a = requestError.config) === null || _a === void 0 ? true : delete _a.httpAgent;
|
|
(_b = requestError.config) === null || _b === void 0 ? true : delete _b.httpsAgent;
|
|
delete requestError.response.request;
|
|
}
|
|
throw requestError;
|
|
}
|
|
finally {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
disposable === null || disposable === void 0 ? void 0 : disposable.dispose();
|
|
}
|
|
}
|
|
traceHeaders(headers) {
|
|
for (const [headerName, headerValue] of Object.entries(headers)) {
|
|
if (headerName === 'Authorization') {
|
|
this.traceAuthorizationHeader(headerName, headerValue);
|
|
return;
|
|
}
|
|
this.trace(`${headerName}: ${headerValue !== null && headerValue !== void 0 ? headerValue : ''}`);
|
|
}
|
|
}
|
|
traceAuthorizationHeader(key, value) {
|
|
if (typeof value !== 'string')
|
|
return;
|
|
const spaceIndex = value.indexOf(' ');
|
|
if (spaceIndex < 0) {
|
|
this.trace(`${key}: [${value.length}]`);
|
|
return;
|
|
}
|
|
const scheme = value.substring(0, spaceIndex);
|
|
const token = value.substring(spaceIndex + 1);
|
|
if (scheme.toLowerCase() === tunnelManagementClient_1.TunnelAuthenticationSchemes.tunnel.toLowerCase()) {
|
|
const tokenProperties = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.tryParse(token);
|
|
if (tokenProperties) {
|
|
this.trace(`${key}: ${scheme} <${tokenProperties}>`);
|
|
return;
|
|
}
|
|
}
|
|
this.trace(`${key}: ${scheme} <token>`);
|
|
}
|
|
traceContent(data) {
|
|
if (typeof data === 'object') {
|
|
data = JSON.stringify(data, undefined, ' ');
|
|
}
|
|
if (typeof data === 'string') {
|
|
this.trace(TunnelManagementHttpClient.replaceTokensInContent(data));
|
|
}
|
|
}
|
|
static replaceTokensInContent(content) {
|
|
var _a;
|
|
const tokenRegex = /"(eyJ[a-zA-z0-9\-_]+\.[a-zA-z0-9\-_]+\.[a-zA-z0-9\-_]+)"/;
|
|
let match = tokenRegex.exec(content);
|
|
while (match) {
|
|
let token = match[1];
|
|
const tokenProperties = tunnelAccessTokenProperties_1.TunnelAccessTokenProperties.tryParse(token);
|
|
token = (_a = tokenProperties === null || tokenProperties === void 0 ? void 0 : tokenProperties.toString()) !== null && _a !== void 0 ? _a : 'token';
|
|
content =
|
|
content.substring(0, match.index + 1) +
|
|
'<' +
|
|
token +
|
|
'>' +
|
|
content.substring(match.index + match[0].length - 1);
|
|
match = tokenRegex.exec(content);
|
|
}
|
|
return content;
|
|
}
|
|
}
|
|
exports.TunnelManagementHttpClient = TunnelManagementHttpClient;
|
|
//# sourceMappingURL=tunnelManagementHttpClient.js.map
|