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
3 changes: 3 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

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.2`

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 if an SSH profile can successfully establish a connection, ensuring quicker troubleshooting of connection issues. [zowe-explorer#3079(comment)](https://github.com/zowe/zowe-explorer-vscode/pull/3079#discussion_r1825783867)

## `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
16 changes: 15 additions & 1 deletion packages/zosuss/__tests__/__system__/Shell.system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ describe("zowe uss issue ssh api call test", () => {
await TestEnvironment.cleanUp(TEST_ENVIRONMENT);
});

describe("Function isConnectionValid", () => {
it("should verify that the connection is valid", async () => {
const response = await Shell.isConnectionValid(SSH_SESSION);
expect(response).toBe(true);
});
it("should verify that the connection is invalid", async () => {
const fakeSession: SshSession = TestEnvironment.createSshSession(TEST_ENVIRONMENT);
fakeSession.ISshSession.hostname = "fake-host";
const response = await Shell.isConnectionValid(fakeSession);
expect(response).toBe(false);
});
});

it ("should execute uname command on the remote system by ssh and return operating system name", async () => {
const command = "uname";
let stdoutData = "";
Expand Down Expand Up @@ -149,7 +162,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
25 changes: 24 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,29 @@ describe("Shell", () => {
checkMockFunctionsWithCommand(command);
});

it("Should execute ssh command with cwd option and no extra characters in the output", async () => {
const cwd = "/";
const command = "commandtest";
await Shell.executeSshCwd(fakeSshSession, command, cwd, stdoutHandler, true);

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
69 changes: 44 additions & 25 deletions packages/zosuss/src/Shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,11 @@ export class Shell {

public static executeSsh(session: SshSession,
command: string,
stdoutHandler: (data: string) => void): Promise<any> {
const authsAllowed = ["none"];
stdoutHandler: (data: string) => void,
removeExtraCharactersFromOutput = false): Promise<any> {
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 +68,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 +93,8 @@ export class Shell {
else if (isUserCommand && dataToPrint.length != 0) {
if (!dataToPrint.startsWith('\r\n$ '+cmd) && !dataToPrint.startsWith('\r<')){
//only prints command output
if (removeExtraCharactersFromOutput && dataToPrint.startsWith("\r\n$ "))
dataToPrint = dataToPrint.replace(/\r\n\$\s/, "\r\n");
stdoutHandler(dataToPrint);
dataToPrint = "";
}
Expand Down Expand Up @@ -140,28 +134,53 @@ 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,
stdoutHandler: (data: string) => void): Promise<any> {
stdoutHandler: (data: string) => void,
removeExtraCharactersFromOutput = false
): Promise<any> {
const cwdCommand = `cd ${cwd} && ${command}`;
return this.executeSsh(session, cwdCommand, stdoutHandler);
return this.executeSsh(session, cwdCommand, stdoutHandler, removeExtraCharactersFromOutput);
}

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[]) {
Expand Down