Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added method to validate the ssh connection #2340

Merged
merged 9 commits into from
Nov 5, 2024
4 changes: 4 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to the Zowe CLI package will be documented in this file.

## Recent Changes

- BugFix: Removed unnecessary `$ ` characters in front of most output. [zowe-explorer#3079(comment)](https://github.com/zowe/zowe-explorer-vscode/pull/3079#pullrequestreview-2408842655)

## `8.6.1`

- BugFix: Fixed an issue where the `zowe zos-logs list logs` command could fail or not return all logs if a start time was not supplied. [#2336](https://github.com/zowe/zowe-cli/pull/2336)
Expand Down
5 changes: 5 additions & 0 deletions packages/zosuss/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to the Zowe z/OS USS SDK package will be documented in this file.

## Recent Changes

- BugFix: Removed unnecessary `$ ` characters in front of most output. [zowe-explorer#3079(comment)](https://github.com/zowe/zowe-explorer-vscode/pull/3079#pullrequestreview-2408842655)
- Enhancement: Added the ability to validate of an SSH profile is able to establish a connection. [zowe-explorer#3079(comment)](https://github.com/zowe/zowe-explorer-vscode/pull/3079#discussion_r1825783867)
zFernand0 marked this conversation as resolved.
Show resolved Hide resolved

## `8.1.1`

- BugFix: Updated peer dependencies to `^8.0.0`, dropping support for versions tagged `next`. [#2287](https://github.com/zowe/zowe-cli/pull/2287)
Expand Down
3 changes: 2 additions & 1 deletion packages/zosuss/__tests__/__system__/Shell.system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ describe("zowe uss issue ssh api call test", () => {
expect(error.toString()).toContain(ZosUssMessages.connectionRefused.message);
} else {
expect(error.toString().includes(ZosUssMessages.allAuthMethodsFailed.message) ||
error.toString().includes(ZosUssMessages.connectionRefused.message)).toBe(true);
error.toString().includes(ZosUssMessages.connectionRefused.message) ||
error.toString().includes(ZosUssMessages.unexpected.message)).toBe(true);
}

}, TIME_OUT);
Expand Down
17 changes: 16 additions & 1 deletion packages/zosuss/__tests__/__unit__/Shell.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const mockShell = jest.fn().mockImplementation((callback) => {
(Client as any).mockImplementation(() => {
mockClient.connect = mockConnect;
mockClient.shell = mockShell;
mockClient.end = jest.fn();
mockClient.end = jest.fn().mockReturnValue(mockClient);
return mockClient;
});

Expand Down Expand Up @@ -103,6 +103,21 @@ describe("Shell", () => {
checkMockFunctionsWithCommand(command);
});

describe("Connection validation", () => {
it("should determine that the connection is valid", async () => {
const response = await Shell.isConnectionValid(fakeSshSession);
expect(response).toBe(true);
});
it("should determine that the connection is invalid", async () => {
mockConnect.mockImplementationOnce(() => {
mockClient.emit("error", new Error(Shell.connRefusedFlag));
mockStream.emit("exit", 0);
});
const response = await Shell.isConnectionValid(fakeSshSession);
expect(response).toBe(false);
});
});

describe("Error handling", () => {
it("should fail when password is expired", async () => {
mockShell.mockImplementationOnce((callback) => {
Expand Down
59 changes: 37 additions & 22 deletions packages/zosuss/src/Shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,9 @@ export class Shell {
public static executeSsh(session: SshSession,
command: string,
stdoutHandler: (data: string) => void): Promise<any> {
const authsAllowed = ["none"];
let hasAuthFailed = false;
const promise = new Promise<any>((resolve, reject) => {
const conn = new Client();

// These are needed for authenticationHandler
// The order is critical as this is the order of authentication that will be used.
if (session.ISshSession.privateKey != null && session.ISshSession.privateKey !== "undefined") {
authsAllowed.push("publickey");
}
if (session.ISshSession.password != null && session.ISshSession.password !== "undefined") {
authsAllowed.push("password");
}
conn.on("ready", () => {
conn.shell((err: any, stream: ClientChannel) => {
if (err) { throw err; }
Expand Down Expand Up @@ -77,6 +67,7 @@ export class Shell {
return;
}
dataBuffer += data;

if (dataBuffer.includes("\r")) {
// when data is not received with complete lines,
// slice the last incomplete line and put it back to dataBuffer until it gets the complete line,
Expand All @@ -101,6 +92,7 @@ export class Shell {
else if (isUserCommand && dataToPrint.length != 0) {
if (!dataToPrint.startsWith('\r\n$ '+cmd) && !dataToPrint.startsWith('\r<')){
//only prints command output
if (dataToPrint.startsWith("\r\n$ ")) dataToPrint = dataToPrint.replace(/\r\n\$\s/, "\r\n");
stdoutHandler(dataToPrint);
dataToPrint = "";
}
Expand Down Expand Up @@ -140,22 +132,37 @@ export class Shell {
}));
}
});
conn.connect({
host: session.ISshSession.hostname,
port: session.ISshSession.port,
username: session.ISshSession.user,
password: session.ISshSession.password,
privateKey: session.ISshSession.privateKey != null && session.ISshSession.privateKey !== "undefined" ?
require("fs").readFileSync(session.ISshSession.privateKey) : "",
passphrase: session.ISshSession.keyPassphrase,
authHandler: this.authenticationHandler(authsAllowed),
readyTimeout: session.ISshSession.handshakeTimeout != null && session.ISshSession.handshakeTimeout !== undefined ?
session.ISshSession.handshakeTimeout : 0
} as any);
Shell.connect(conn, session);
});
return promise;
}

private static connect(connection: Client, session: SshSession) {
const authsAllowed = ["none"];

// These are needed for authenticationHandler
// The order is critical as this is the order of authentication that will be used.
if (session.ISshSession.privateKey != null && session.ISshSession.privateKey !== "undefined") {
authsAllowed.push("publickey");
}
if (session.ISshSession.password != null && session.ISshSession.password !== "undefined") {
authsAllowed.push("password");
}

connection.connect({
host: session.ISshSession.hostname,
port: session.ISshSession.port,
username: session.ISshSession.user,
password: session.ISshSession.password,
privateKey: session.ISshSession.privateKey != null && session.ISshSession.privateKey !== "undefined" ?
require("fs").readFileSync(session.ISshSession.privateKey) : "",
passphrase: session.ISshSession.keyPassphrase,
authHandler: this.authenticationHandler(authsAllowed),
readyTimeout: session.ISshSession.handshakeTimeout != null && session.ISshSession.handshakeTimeout !== undefined ?
session.ISshSession.handshakeTimeout : 0
} as any);
}

public static async executeSshCwd(session: SshSession,
command: string,
cwd: string,
Expand All @@ -164,6 +171,14 @@ export class Shell {
return this.executeSsh(session, cwdCommand, stdoutHandler);
}

public static async isConnectionValid(session: SshSession): Promise<boolean>{
zFernand0 marked this conversation as resolved.
Show resolved Hide resolved
return new Promise((resolve, _) => {
const conn = new Client();
conn.on("ready", () => conn.end() && resolve(true)).on("error", () => resolve(false));
Shell.connect(conn, session);
});
}

private static authenticationHandler(authsAllowed: string[]) {
let authPos = 0;
return (methodsLeft: string[], partialSuccess: boolean, callback: any) => {
Expand Down
Loading