UNPKG

@microsoft/dev-tunnels-connections

Version:

Tunnels library for Visual Studio tools

277 lines 10.8 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. Object.defineProperty(exports, "__esModule", { value: true }); exports.RelayConnectionError = exports.RelayErrorType = exports.isNode = exports.SshHelpers = exports.BrowserWebSocketRelayError = void 0; const ssh = require("@microsoft/dev-tunnels-ssh"); const websocket_1 = require("websocket"); /** * Error class for errors connecting to a web socket in non-node (browser) context. * There is no status code or underlying network error info in the browser context. */ class BrowserWebSocketRelayError extends Error { constructor(message) { super(message); } } exports.BrowserWebSocketRelayError = BrowserWebSocketRelayError; /** * Ssh connection helper */ class SshHelpers { /** * Open a connection to the relay uri depending on the running environment. * @param relayUri * @param protocols * @param headers * @param clientConfig * @returns */ static openConnection(relayUri, protocols, headers, clientConfig) { if ((0, exports.isNode)()) { return SshHelpers.nodeSshStreamFactory(relayUri, protocols, headers, clientConfig); } return SshHelpers.webSshStreamFactory(new WebSocket(relayUri, protocols)); } /** * Creates a client SSH session with standard configuration for tunnels. * @param configure Optional callback for additional session configuration. * @returns The created SSH session. */ static createSshClientSession(configure) { return SshHelpers.createSshSession((config) => { if (configure) configure(config); return new ssh.SshClientSession(config); }); } /** * Creates a SSH server session with standard configuration for tunnels. * @param reconnectableSessions Optional list that tracks reconnectable sessions. * @param configure Optional callback for additional session configuration. * @returns The created SSH session. */ static createSshServerSession(reconnectableSessions, configure) { return SshHelpers.createSshSession((config) => { if (configure) configure(config); return new ssh.SshServerSession(config, reconnectableSessions); }); } /** * Create a websocketStream from a connection. * @param connection * @returns */ static createWebSocketStreamAdapter(connection) { return new ssh.WebSocketStream(new WebsocketStreamAdapter(connection)); } /** * Set up a web Ssh stream factory. * @param socket * @returns */ static webSshStreamFactory(socket) { socket.binaryType = 'arraybuffer'; return new Promise((resolve, reject) => { const relayError = 'Failed to connect to relay url'; socket.onopen = () => { resolve(new ssh.WebSocketStream(socket)); }; socket.onerror = (e) => { // Note: as per web socket guidance https://websockets.spec.whatwg.org/#eventdef-websocket-error, // the user agents must not convey extended error information including the cases where the server // didn't complete the opening handshake (e.g. because it was not a WebSocket server). // So we cannot obtain the response status code. // Note: When the socket is connected and an error occurs then `onclose` event occurs after `onerror`. // However, the promise is already rejected by `onerror` and we loose this information, hence the // timeout helps to give us some info in this scenario. setTimeout(() => reject(new BrowserWebSocketRelayError(relayError)), 100); }; socket.onclose = (e) => { if (e.code !== 1000) { reject(new BrowserWebSocketRelayError(`${relayError} Code: ${e.code} Reason: ${e.reason}`)); } }; }); } static createSshSession(factoryCallback) { const config = new ssh.SshSessionConfiguration(); config.keyExchangeAlgorithms.splice(0); config.keyExchangeAlgorithms.push(ssh.SshAlgorithms.keyExchange.ecdhNistp384Sha384); config.keyExchangeAlgorithms.push(ssh.SshAlgorithms.keyExchange.ecdhNistp256Sha256); config.keyExchangeAlgorithms.push(ssh.SshAlgorithms.keyExchange.dhGroup14Sha256); return factoryCallback(config); } static nodeSshStreamFactory(relayUri, protocols, headers, clientConfig) { const client = new websocket_1.client(clientConfig); return new Promise((resolve, reject) => { client.on('connect', (connection) => { resolve(new ssh.WebSocketStream(new WebsocketStreamAdapter(connection))); }); // If the server responds but doesn't properly upgrade the connection to web socket, WebSocketClient fires 'httpResponse' event. // TODO: Return ProblemDetails from TunnelRelay service client.on('httpResponse', ({ statusCode, statusMessage }) => { var _a; const errorContext = (_a = webSocketClientContexts.find((c) => c.statusCode === statusCode)) !== null && _a !== void 0 ? _a : { statusCode, errorType: RelayErrorType.ServerError, error: `relayConnectionError Server responded with a non-101 status: ${statusCode} ${statusMessage}`, }; reject(new RelayConnectionError(`error.${errorContext.error}`, errorContext)); }); // All other failure cases - cannot connect and get the response, or the web socket handshake failed. client.on('connectFailed', ({ message }) => { var _a; if (message && message.startsWith('Error: ')) { message = message.substr(7); } const errorContext = (_a = webSocketClientContexts.find((c) => c.regex && c.regex.test(message))) !== null && _a !== void 0 ? _a : { // Other errors are most likely connectivity issues. // The original error message may have additional helpful details. errorType: RelayErrorType.ServerError, error: `relayConnectionError ${message}`, }; reject(new RelayConnectionError(`error.${errorContext.error}`, errorContext)); }); client.connect(relayUri, protocols, undefined, headers); }); } } exports.SshHelpers = SshHelpers; /** * Partially adapts a Node websocket connection object to the browser websocket API, * enough so that it can be used as an SSH stream. */ class WebsocketStreamAdapter { constructor(connection) { this.connection = connection; } get protocol() { return this.connection.protocol; } set onmessage(messageHandler) { if (messageHandler) { this.connection.on('message', (message) => { // This assumes all messages are binary. messageHandler({ data: message.binaryData }); }); } else { // Removing event handlers is not implemented. } } set onclose(closeHandler) { if (closeHandler) { this.connection.on('close', (code, reason) => { closeHandler({ code, reason, wasClean: !(code || reason) }); }); } else { // Removing event handlers is not implemented. } } send(data) { if (Buffer.isBuffer(data)) { this.connection.sendBytes(data); } else { this.connection.sendBytes(Buffer.from(data)); } } close(code, reason) { if (code || reason) { this.connection.drop(code, reason); } else { this.connection.close(); } } } /** * Helper function to check the running environment. */ const isNode = () => typeof process !== 'undefined' && typeof process.release !== 'undefined' && process.release.name === 'node'; exports.isNode = isNode; /** * Type of relay connection error types. */ var RelayErrorType; (function (RelayErrorType) { RelayErrorType[RelayErrorType["ConnectionError"] = 1] = "ConnectionError"; RelayErrorType[RelayErrorType["Unauthorized"] = 2] = "Unauthorized"; /** * @deprecated This relay error type is not used. */ RelayErrorType[RelayErrorType["EndpointNotFound"] = 3] = "EndpointNotFound"; /** * @deprecated This relay error type is not used. */ RelayErrorType[RelayErrorType["ListenerOffline"] = 4] = "ListenerOffline"; RelayErrorType[RelayErrorType["ServerError"] = 5] = "ServerError"; RelayErrorType[RelayErrorType["TunnelPortNotFound"] = 6] = "TunnelPortNotFound"; RelayErrorType[RelayErrorType["TooManyRequests"] = 7] = "TooManyRequests"; RelayErrorType[RelayErrorType["ServiceUnavailable"] = 8] = "ServiceUnavailable"; RelayErrorType[RelayErrorType["BadGateway"] = 9] = "BadGateway"; })(RelayErrorType = exports.RelayErrorType || (exports.RelayErrorType = {})); /** * Error used when a connection to the tunnel relay failed. */ class RelayConnectionError extends Error { constructor(message, errorContext) { super(message); this.errorContext = errorContext; } } exports.RelayConnectionError = RelayConnectionError; /** * Web socket client error contexts. */ // TODO: Return ProblemDetails from TunnelRelay service. const webSocketClientContexts = [ { regex: /status: 401/, statusCode: 401, error: 'relayClientUnauthorized', errorType: RelayErrorType.Unauthorized, }, { regex: /status: 403/, statusCode: 403, error: 'relayClientForbidden', errorType: RelayErrorType.Unauthorized, }, { regex: /status: 404/, statusCode: 404, error: 'tunnelPortNotFound', errorType: RelayErrorType.TunnelPortNotFound, }, { regex: /status: 429/, statusCode: 429, error: 'tooManyRequests', errorType: RelayErrorType.TooManyRequests, }, { regex: /status: 500/, statusCode: 500, error: 'relayServerError', errorType: RelayErrorType.ServerError, }, { regex: /status: 502/, statusCode: 502, error: 'badGateway', errorType: RelayErrorType.BadGateway, }, { regex: /status: 503/, statusCode: 503, error: 'serviceUnavailable', errorType: RelayErrorType.ServiceUnavailable, }, ]; //# sourceMappingURL=sshHelpers.js.map