@microsoft/dev-tunnels-connections
Version:
Tunnels library for Visual Studio tools
277 lines • 10.8 kB
JavaScript
;
// 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