From 5a7a06e5296c06cb2e3b31f2a3e6987656d0bd0a Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 16 Jun 2025 23:12:36 +0200 Subject: [PATCH] fixed #10034 - added auto-sudo-password plugin --- scripts/vars.mjs | 1 + tabby-auto-sudo-password/package.json | 23 +++++ tabby-auto-sudo-password/src/decorator.ts | 89 +++++++++++++++++++ tabby-auto-sudo-password/src/index.ts | 16 ++++ tabby-auto-sudo-password/tsconfig.json | 7 ++ .../tsconfig.typings.json | 14 +++ tabby-auto-sudo-password/webpack.config.mjs | 10 +++ tabby-auto-sudo-password/yarn.lock | 8 ++ tabby-ssh/src/components/sshTab.component.ts | 1 + tabby-ssh/src/index.ts | 3 +- 10 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 tabby-auto-sudo-password/package.json create mode 100644 tabby-auto-sudo-password/src/decorator.ts create mode 100644 tabby-auto-sudo-password/src/index.ts create mode 100644 tabby-auto-sudo-password/tsconfig.json create mode 100644 tabby-auto-sudo-password/tsconfig.typings.json create mode 100644 tabby-auto-sudo-password/webpack.config.mjs create mode 100644 tabby-auto-sudo-password/yarn.lock diff --git a/scripts/vars.mjs b/scripts/vars.mjs index 218402f8..13660499 100755 --- a/scripts/vars.mjs +++ b/scripts/vars.mjs @@ -31,6 +31,7 @@ export const builtinPlugins = [ 'tabby-electron', 'tabby-plugin-manager', 'tabby-linkifier', + 'tabby-auto-sudo-password', ] export const packagesWithDocs = [ diff --git a/tabby-auto-sudo-password/package.json b/tabby-auto-sudo-password/package.json new file mode 100644 index 00000000..c872424e --- /dev/null +++ b/tabby-auto-sudo-password/package.json @@ -0,0 +1,23 @@ +{ + "name": "tabby-auto-sudo-password", + "version": "1.0.197-nightly.1", + "description": "Offers to automatically paste saved sudo password in SSH sessions", + "keywords": [ + "tabby-builtin-plugin" + ], + "main": "dist/index.js", + "typings": "typings/index.d.ts", + "scripts": { + "build": "webpack --progress --color --display-modules", + "watch": "webpack --progress --color --watch" + }, + "files": [ + "dist", + "typings" + ], + "devDependencies": { + "ansi-colors": "^4.1.1" + }, + "author": "Eugene Pankov", + "license": "MIT" +} diff --git a/tabby-auto-sudo-password/src/decorator.ts b/tabby-auto-sudo-password/src/decorator.ts new file mode 100644 index 00000000..a2167e90 --- /dev/null +++ b/tabby-auto-sudo-password/src/decorator.ts @@ -0,0 +1,89 @@ +import colors from 'ansi-colors' +import { Injectable } from '@angular/core' +import { TerminalDecorator, BaseTerminalTabComponent, XTermFrontend, SessionMiddleware } from 'tabby-terminal' +import { SSHProfile, SSHTabComponent, PasswordStorageService } from 'tabby-ssh' + +const SUDO_PROMPT_REGEX = /^\[sudo\] password for ([^:]+):\s*$/im + +export class AutoSudoPasswordMiddleware extends SessionMiddleware { + private pendingPasswordToPaste: string | null = null + private pasteHint = `${colors.black.bgBlackBright(' Tabby ')} ${colors.gray('Press Enter to paste saved password')}` + private pasteHintLength = colors.stripColor(this.pasteHint).length + + constructor ( + private profile: SSHProfile, + private ps: PasswordStorageService, + ) { super() } + + feedFromSession (data: Buffer): void { + const text = data.toString('utf-8') + const match = SUDO_PROMPT_REGEX.exec(text) + if (match) { + const username = match[1] + this.handlePrompt(username) + } + this.outputToTerminal.next(data) + } + + feedFromTerminal (data: Buffer): void { + if (this.pendingPasswordToPaste) { + const backspaces = Buffer.alloc(this.pasteHintLength, 8) // backspace + const spaces = Buffer.alloc(this.pasteHintLength, 32) // space + const clear = Buffer.concat([backspaces, spaces, backspaces]) + this.outputToTerminal.next(clear) + if (data.length === 1 && data[0] === 13) { // Enter key + this.outputToSession.next(Buffer.from(this.pendingPasswordToPaste + '\n')) + this.pendingPasswordToPaste = null + return + } else { + this.pendingPasswordToPaste = null + } + } + this.outputToSession.next(data) + } + + async handlePrompt (username: string): Promise { + console.log(`Detected sudo prompt for user: ${username}`) + const pw = await this.ps.loadPassword(this.profile) + if (pw) { + this.outputToTerminal.next(Buffer.from(this.pasteHint)) + this.pendingPasswordToPaste = pw + } + } + + async loadPassword (username: string): Promise { + if (this.profile.options.user !== username) { + return null + } + return this.ps.loadPassword(this.profile) + } +} + +@Injectable() +export class AutoSudoPasswordDecorator extends TerminalDecorator { + constructor ( + private ps: PasswordStorageService, + ) { + super() + } + + private attachToSession (tab: SSHTabComponent) { + if (!tab.session) { + return + } + tab.session.middleware.unshift(new AutoSudoPasswordMiddleware(tab.profile, this.ps)) + } + + attach (tab: BaseTerminalTabComponent): void { + if (!(tab.frontend instanceof XTermFrontend) || !(tab instanceof SSHTabComponent)) { + return + } + + setTimeout(() => { + this.attachToSession(tab) + this.subscribeUntilDetached(tab, tab.sessionChanged$.subscribe(() => { + this.attachToSession(tab) + })) + }) + } +} diff --git a/tabby-auto-sudo-password/src/index.ts b/tabby-auto-sudo-password/src/index.ts new file mode 100644 index 00000000..6120afd2 --- /dev/null +++ b/tabby-auto-sudo-password/src/index.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ +import { NgModule } from '@angular/core' +import { ToastrModule } from 'ngx-toastr' +import { TerminalDecorator } from 'tabby-terminal' + +import { AutoSudoPasswordDecorator } from './decorator' + +@NgModule({ + imports: [ + ToastrModule, + ], + providers: [ + { provide: TerminalDecorator, useClass: AutoSudoPasswordDecorator, multi: true }, + ], +}) +export default class AutoSudoPasswordModule { } diff --git a/tabby-auto-sudo-password/tsconfig.json b/tabby-auto-sudo-password/tsconfig.json new file mode 100644 index 00000000..e0611107 --- /dev/null +++ b/tabby-auto-sudo-password/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "baseUrl": "src", + } +} diff --git a/tabby-auto-sudo-password/tsconfig.typings.json b/tabby-auto-sudo-password/tsconfig.typings.json new file mode 100644 index 00000000..9188bdf7 --- /dev/null +++ b/tabby-auto-sudo-password/tsconfig.typings.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "exclude": ["node_modules", "dist", "typings"], + "compilerOptions": { + "baseUrl": "src", + "emitDeclarationOnly": true, + "declaration": true, + "declarationDir": "./typings", + "paths": { + "tabby-*": ["../../tabby-*"], + "*": ["../../app/node_modules/*"] + } + } +} diff --git a/tabby-auto-sudo-password/webpack.config.mjs b/tabby-auto-sudo-password/webpack.config.mjs new file mode 100644 index 00000000..e84b6701 --- /dev/null +++ b/tabby-auto-sudo-password/webpack.config.mjs @@ -0,0 +1,10 @@ +import * as path from 'path' +import * as url from 'url' +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +import config from '../webpack.plugin.config.mjs' + +export default () => config({ + name: 'auto-sudo-password', + dirname: __dirname, +}) diff --git a/tabby-auto-sudo-password/yarn.lock b/tabby-auto-sudo-password/yarn.lock new file mode 100644 index 00000000..647c79cf --- /dev/null +++ b/tabby-auto-sudo-password/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== diff --git a/tabby-ssh/src/components/sshTab.component.ts b/tabby-ssh/src/components/sshTab.component.ts index 54bb8c0b..c662f4a4 100644 --- a/tabby-ssh/src/components/sshTab.component.ts +++ b/tabby-ssh/src/components/sshTab.component.ts @@ -179,6 +179,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent try { await this.initializeSessionMaybeMultiplex(false) } catch (e) { + console.error('SSH session initialization failed', e) this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n') return } diff --git a/tabby-ssh/src/index.ts b/tabby-ssh/src/index.ts index fa5f529d..01199e39 100644 --- a/tabby-ssh/src/index.ts +++ b/tabby-ssh/src/index.ts @@ -66,4 +66,5 @@ export default class SSHModule { } export * from './api' export { SFTPFile, SFTPSession } from './session/sftp' -export { SFTPPanelComponent } +export { SFTPPanelComponent, SSHTabComponent } +export { PasswordStorageService } from './services/passwordStorage.service'