Skip to content

Commit

Permalink
Merge pull request #2340 from zowe/ze/iTerm
Browse files Browse the repository at this point in the history
feat: added method to validate the ssh connection
  • Loading branch information
zFernand0 authored Nov 5, 2024
2 parents 3e43937 + deea16f commit c0f2fc0
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 27 deletions.
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>{
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

0 comments on commit c0f2fc0

Please sign in to comment.