"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} `); } 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