Skip to content

Commit

Permalink
Merge branch 'ssh_agent'
Browse files Browse the repository at this point in the history
  • Loading branch information
sedwards2009 committed Jul 16, 2024
2 parents 217de5f + 6d6142a commit 2645cce
Show file tree
Hide file tree
Showing 21 changed files with 389 additions and 130 deletions.
3 changes: 3 additions & 0 deletions extensions/SSHQuickOpen/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: "extraterm"
};
1 change: 1 addition & 0 deletions extensions/SSHQuickOpen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Quick Open SSH session
42 changes: 42 additions & 0 deletions extensions/SSHQuickOpen/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "ssh-quick-open",
"description": "SSH Quick Open",
"author": "Simon Edwards",
"license": "MIT",
"version": "1.0.0",
"type": "module",
"exports": "./dist/SSHQuickOpenExtension.cjs",
"scripts": {
"build": "yarn run build-code && yarn run build-bundle && yarn run lint",
"build-code": "tsc",
"build-bundle": "esbuild build/SSHQuickOpenExtension.js --bundle --outfile=dist/SSHQuickOpenExtension.cjs --platform=node --format=cjs --external:@nodegui/nodegui \"--external:nodegui-plugin-*\"",
"clean": "shx rm -rf build dist",
"lint": "eslint \"src/**/*.ts\"",
"lint-strict": "eslint --max-warnings 1 \"src/**/*.ts\""
},
"devDependencies": {
"@extraterm/extraterm-extension-api": "0.15.0",
"@types/fs-extra": "^5.0.2",
"@types/lodash-es": "4.17.10",
"@types/node": "^18.15.3",
"esbuild": "^0.15.5",
"eslint": "8.53.0",
"eslint-config-extraterm": "1.0.0",
"eslint-plugin-unicorn": "42.0.0",
"extraterm-uuid": "1.0.0",
"qt-construct": "0.1.0",
"shx": "^0.3.2",
"typescript": "5.2.2"
},
"isInternal": false,
"contributes": {
"commands": [
{
"command": "ssh-quick-open:open",
"title": "SSH Quick Open",
"category": "terminal",
"icon": "fa-plug"
}
]
}
}
71 changes: 71 additions & 0 deletions extensions/SSHQuickOpen/src/SSHQuickOpenExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2024 Simon Edwards <[email protected]>
*
* This source code is licensed under the MIT license which is detailed in the LICENSE.txt file.
*/
import { ExtensionContext, Logger, SessionConfiguration } from '@extraterm/extraterm-extension-api';
import { createUuid } from "extraterm-uuid";


let log: Logger = null;
let context: ExtensionContext = null;


export function activate(_context: ExtensionContext): any {
context = _context;
log = context.logger;
context.commands.registerCommand("ssh-quick-open:open", quickOpenCommand);
}

// Note: This is mostly duplicated in SSHSessionEditorExtension.ts.
interface SSHSessionConfiguration extends SessionConfiguration {
host?: string;
port?: number;
username?: string;
authenticationMethod?: number; // AuthenticationMethod;
}

async function quickOpenCommand(): Promise<void> {
const sshConnectionString = await context.activeTab.showTextInput({
message: "Enter a SSH connection string:",
value: "",
});
if (sshConnectionString == null) {
return;
}

const sshSessionConfiguration = parseConnectionString(sshConnectionString);
context.commands.executeCommand("extraterm:window.newTerminal", {sessionConfiguration: sshSessionConfiguration});
}

function parseConnectionString(sshConnectionString: string): SSHSessionConfiguration {
let username: string = null;

const parts = sshConnectionString.split("@");
let hostnamePort = sshConnectionString;
if (parts.length === 2) {
username = parts[0];
hostnamePort = parts[1];
}

const hostPortParts = hostnamePort.split(":");
let host: string = hostnamePort;
let port = 22;
if (hostPortParts.length === 2) {
host = hostPortParts[0];
const parsedPort = parseInt(hostPortParts[1], 10);
if (! isNaN(parsedPort)) {
port = parsedPort;
}
}

return {
uuid: createUuid(),
name: sshConnectionString,
type: "ssh",
authenticationMethod: 0,
host,
port,
username,
};
}
10 changes: 10 additions & 0 deletions extensions/SSHQuickOpen/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "build",
"rootDir": "src"
},
"include": [
"./src/**/*.ts"
]
}
10 changes: 10 additions & 0 deletions extensions/SSHSessionBackend/src/SSHPty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface PtyOptions {
username: string;
privateKeyFilenames?: string[];
tryPasswordAuth: boolean;
agentSocketPath?: string;
verboseLogging?: boolean;
}

Expand All @@ -49,6 +50,7 @@ export class SSHPty implements Pty {
#password: string = "";

#ptyOptions: PtyOptions = null;
#tryAgentAuth = false;
#verifyCallback: ssh2.VerifyCallback = null;

#permittedDataSize = 0;
Expand Down Expand Up @@ -155,6 +157,7 @@ export class SSHPty implements Pty {
});
});

this.#tryAgentAuth = options.agentSocketPath !== undefined;
let debugFunction: ssh2.DebugFunction = undefined;
if (options.verboseLogging) {
debugFunction = (message: string): void => {
Expand All @@ -168,6 +171,7 @@ export class SSHPty implements Pty {
port: options.port,
username: options.username,
tryKeyboard: false,
agent: options.agentSocketPath,
authHandler: (
methodsLeft: ssh2.AuthenticationType[],
partialSuccess: boolean,
Expand Down Expand Up @@ -368,6 +372,12 @@ export class SSHPty implements Pty {
partialSuccess: boolean,
callback: ssh2.NextAuthHandler): void {

if (this.#tryAgentAuth) {
this.#tryAgentAuth = false;
callback({type: "agent", agent: this.#ptyOptions.agentSocketPath, username: this.#ptyOptions.username});
return;
}

while (this.#remainingPrivateKeyFilenames.length !== 0) {
const keyFilename = this.#remainingPrivateKeyFilenames.pop();
if (this.#handlePrivateKeyAuth(keyFilename, callback)) {
Expand Down
35 changes: 30 additions & 5 deletions extensions/SSHSessionBackend/src/SSHSessionBackendExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* This source code is licensed under the MIT license which is detailed in the LICENSE.txt file.
*/
import * as child_process from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";

Expand All @@ -16,19 +17,22 @@ import { PtyOptions, SSHPty } from "./SSHPty";
enum AuthenticationMethod {
DEFAULT_KEYS_PASSWORD,
PASSWORD_ONLY,
KEY_FILE_ONLY
KEY_FILE_ONLY,
SSH_AGENT_ONLY,
};

// Note: This is duplicated in SSHSessionEditorExtension.ts.
interface SSHSessionConfiguration extends SessionConfiguration {
host?: string;
port?: number;
username?: string;
authenicationMethod?: AuthenticationMethod;
authenticationMethod?: AuthenticationMethod;
keyFilePath?: string;
verboseLogging?: boolean;
}

const WINDOW_SSH_AUTH_SOCK = "\\\\.\\pipe\\openssh-ssh-agent";

class SSHBackend implements SessionBackend {

constructor(private _log: Logger) {
Expand All @@ -54,7 +58,7 @@ class SSHBackend implements SessionBackend {

const privateKeyFilenames: string[] = [];
let tryPasswordAuth = false;
switch (sessionConfig.authenicationMethod) {
switch (sessionConfig.authenticationMethod) {
case AuthenticationMethod.DEFAULT_KEYS_PASSWORD:
const homeDir = os.homedir();
privateKeyFilenames.push(path.join(homeDir, ".ssh", "id_rsa"));
Expand All @@ -64,12 +68,15 @@ class SSHBackend implements SessionBackend {
tryPasswordAuth = true;
break;

case AuthenticationMethod.KEY_FILE_ONLY:
privateKeyFilenames.push(sessionConfig.keyFilePath);
break;

case AuthenticationMethod.PASSWORD_ONLY:
tryPasswordAuth = true;
break;

case AuthenticationMethod.KEY_FILE_ONLY:
privateKeyFilenames.push(sessionConfig.keyFilePath);
default:
break;
}

Expand All @@ -83,12 +90,30 @@ class SSHBackend implements SessionBackend {
username: username,
privateKeyFilenames,
tryPasswordAuth,
agentSocketPath: this.#createAgentSocketPath(sessionConfig),
verboseLogging: sessionConfig.verboseLogging,
};

return new SSHPty(this._log, options);
}

#createAgentSocketPath(sessionConfig: SSHSessionConfiguration): string {
const needAgent = sessionConfig.authenticationMethod === AuthenticationMethod.DEFAULT_KEYS_PASSWORD ||
sessionConfig.authenticationMethod === AuthenticationMethod.SSH_AGENT_ONLY;
if (! needAgent) {
return undefined;
}

if (process.platform === "win32") {
if (fs.existsSync(WINDOW_SSH_AUTH_SOCK)) {
return WINDOW_SSH_AUTH_SOCK;
}
return undefined;
} else {
return process.env.SSH_AUTH_SOCK;
}
}

#createEnv(sessionOptions: CreateSessionOptions): EnvironmentMap {
const ptyEnv: EnvironmentMap = {};
const processEnv = process.env;
Expand Down
20 changes: 13 additions & 7 deletions extensions/SSHSessionEditor/src/SSHSessionEditorExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@ let log: Logger = null;
enum AuthenticationMethod {
DEFAULT_KEYS_PASSWORD,
PASSWORD_ONLY,
KEY_FILE_ONLY
KEY_FILE_ONLY,
SSH_AGENT_ONLY,
};

const AUTHENTICATION_METHOD_LABELS = ["Default OpenSSH keys, Password", "Password only", "Key file only"];
const AUTHENTICATION_METHOD_LABELS = [
"SSH Agent, Default OpenSSH keys, Password",
"Password only",
"Key file only",
"SSH Agent only"
];

// Note: This is duplicated in SSHSessionBackendExtension.ts.
interface SSHSessionConfiguration extends SessionConfiguration {
host?: string;
port?: number;
username?: string;
authenicationMethod?: AuthenticationMethod;
authenticationMethod?: AuthenticationMethod;
keyFilePath?: string;
verboseLogging?: boolean;
}
Expand Down Expand Up @@ -102,10 +108,10 @@ class EditorUi {

"Authentication:",
ComboBox({
currentIndex: this.#config.authenicationMethod ?? AuthenticationMethod.DEFAULT_KEYS_PASSWORD,
currentIndex: this.#config.authenticationMethod ?? AuthenticationMethod.DEFAULT_KEYS_PASSWORD,
items: AUTHENTICATION_METHOD_LABELS,
onCurrentIndexChanged: (index: number): void => {
this.#config.authenicationMethod = index;
this.#config.authenticationMethod = index;
sessionEditorBase.setSessionConfiguration(this.#config);
this.#updateKeyFileLabel();
}
Expand Down Expand Up @@ -133,7 +139,7 @@ class EditorUi {
onClicked: (): void => {
this.#handleSelectKeyFile();
},
enabled: this.#config.authenicationMethod === AuthenticationMethod.KEY_FILE_ONLY,
enabled: this.#config.authenticationMethod === AuthenticationMethod.KEY_FILE_ONLY,
}),
stretch: 0,
}
Expand All @@ -157,7 +163,7 @@ class EditorUi {

#updateKeyFileLabel(): void {
this.#selectedKeyFileLineEdit.setText(this.#config.keyFilePath ?? "");
const enabled = this.#config.authenicationMethod === AuthenticationMethod.KEY_FILE_ONLY;
const enabled = this.#config.authenticationMethod === AuthenticationMethod.KEY_FILE_ONLY;
this.#selectedKeyFileLineEdit.setEnabled(enabled);
this.#selectKeyFileButton.setEnabled(enabled);
}
Expand Down
2 changes: 2 additions & 0 deletions main/src/InternalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export interface ExtensionManager {
createSessionSettingsEditors(sessionType: string, sessionConfiguration: ExtensionApi.SessionConfiguration,
window: Window): InternalSessionSettingsEditor[];
showDialog(tab: Tab, options: ExtensionApi.DialogOptions): Promise<number | undefined>;
showTextInput(tab: Tab, options: ExtensionApi.TextInputOptions): Promise<string | undefined>;
showListPicker(tab: Tab, options: ExtensionApi.ListPickerOptions): Promise<number>;
showOnCursorListPicker(terminal: Terminal, options: ExtensionApi.ListPickerOptions): Promise<number>;
getSettingsTabContributions(): LoadedSettingsTabContribution[];
Expand Down Expand Up @@ -131,6 +132,7 @@ export interface InternalExtensionContext extends ExtensionApi.Disposable {
newWindowCreated(window: Window): void;

showDialog(tab: Tab, options: ExtensionApi.DialogOptions): Promise<number | undefined>;
showTextInput(tab: Tab, options: ExtensionApi.TextInputOptions): Promise<string | undefined>;
showListPicker(tab: Tab, options: ExtensionApi.ListPickerOptions): Promise<number>;
showOnCursorListPicker(terminal: Terminal, options: ExtensionApi.ListPickerOptions): Promise<number>;

Expand Down
10 changes: 10 additions & 0 deletions main/src/extension/ExtensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { InternalExtensionContextImpl } from "./InternalExtensionContextImpl.js"
import { Tab } from "../Tab.js";
import { ListPickerPopOver } from "./ListPickerPopOver.js";
import { DialogPopOver } from "./DialogPopOver.js";
import { TextInputPopOver } from "./TextInputPopOver.js";
import { UiStyle } from "../ui/UiStyle.js";
import { BlockFrame } from "../terminal/BlockFrame.js";
import { TerminalBlock } from "../terminal/TerminalBlock.js";
Expand Down Expand Up @@ -91,6 +92,7 @@ export class ExtensionManager implements InternalTypes.ExtensionManager {

#listPickerPopOver: ListPickerPopOver = null;
#dialogPopOver: DialogPopOver = null;
#textInputPopOver: TextInputPopOver = null;

constructor(configDatabase: ConfigDatabase, themeManager: ThemeManager, extensionPaths: string[],
applicationVersion: string) {
Expand Down Expand Up @@ -887,6 +889,14 @@ export class ExtensionManager implements InternalTypes.ExtensionManager {
return this.#listPickerPopOver.show(win, {...options, containingRect: win.getTabGlobalGeometry(tab)});
}

async showTextInput(tab: Tab, options: ExtensionApi.TextInputOptions): Promise<string | undefined> {
if (this.#textInputPopOver == null) {
this.#textInputPopOver = new TextInputPopOver(this.#uiStyle);
}
const win = this.getWindowForTab(tab);
return this.#textInputPopOver.show(win, {...options, containingRect: win.getTabGlobalGeometry(tab)});
}

async showOnCursorListPicker(terminal: Terminal, options: ExtensionApi.ListPickerOptions): Promise<number> {
if (this.#listPickerPopOver == null) {
this.#listPickerPopOver = new ListPickerPopOver(this.#uiStyle);
Expand Down
4 changes: 4 additions & 0 deletions main/src/extension/InternalExtensionContextImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ export class InternalExtensionContextImpl implements InternalExtensionContext {
return this.#extensionManager.showListPicker(tab, options);
}

async showTextInput(tab: Tab, options: ExtensionApi.TextInputOptions): Promise<string | undefined> {
return this.#extensionManager.showTextInput(tab, options);
}

async showOnCursorListPicker(terminal: Terminal, options: ExtensionApi.ListPickerOptions): Promise<number> {
return this.#extensionManager.showOnCursorListPicker(terminal, options);
}
Expand Down
Loading

0 comments on commit 2645cce

Please sign in to comment.