reworked login scripts - fixes #1349

This commit is contained in:
Eugene Pankov 2021-07-13 20:19:28 +02:00
parent 5053743b1b
commit 9502240480
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
11 changed files with 117 additions and 76 deletions

View File

@ -17,6 +17,7 @@
"@types/fs-extra": "^9.0.12", "@types/fs-extra": "^9.0.12",
"@types/js-yaml": "^4.0.2", "@types/js-yaml": "^4.0.2",
"@types/node": "16.0.1", "@types/node": "16.0.1",
"@types/sortablejs": "^1.10.7",
"@types/webpack-env": "^1.16.2", "@types/webpack-env": "^1.16.2",
"@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2", "@typescript-eslint/parser": "^4.28.2",
@ -40,6 +41,7 @@
"json-loader": "0.5.7", "json-loader": "0.5.7",
"lru-cache": "^6.0.0", "lru-cache": "^6.0.0",
"macos-release": "^2.5.0", "macos-release": "^2.5.0",
"ngx-sortablejs": "^11.1.0",
"ngx-toastr": "^14.0.0", "ngx-toastr": "^14.0.0",
"node-abi": "^2.30.0", "node-abi": "^2.30.0",
"node-sass": "^6.0.1", "node-sass": "^6.0.1",
@ -55,6 +57,7 @@
"sass-loader": "^12.1.0", "sass-loader": "^12.1.0",
"shelljs": "0.8.4", "shelljs": "0.8.4",
"slugify": "^1.5.3", "slugify": "^1.5.3",
"sortablejs": "^1.14.0",
"source-code-pro": "^2.38.0", "source-code-pro": "^2.38.0",
"source-map-loader": "^3.0.0", "source-map-loader": "^3.0.0",
"source-sans-pro": "3.6.0", "source-sans-pro": "3.6.0",

View File

@ -6,6 +6,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar' import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
import { NgxFilesizeModule } from 'ngx-filesize' import { NgxFilesizeModule } from 'ngx-filesize'
import { DndModule } from 'ng2-dnd' import { DndModule } from 'ng2-dnd'
import { SortablejsModule } from 'ngx-sortablejs'
import { AppRootComponent } from './components/appRoot.component' import { AppRootComponent } from './components/appRoot.component'
import { CheckboxComponent } from './components/checkbox.component' import { CheckboxComponent } from './components/checkbox.component'
@ -77,6 +78,7 @@ const PROVIDERS = [
NgxFilesizeModule, NgxFilesizeModule,
PerfectScrollbarModule, PerfectScrollbarModule,
DndModule.forRoot(), DndModule.forRoot(),
SortablejsModule.forRoot({ animation: 150 }),
], ],
declarations: [ declarations: [
AppRootComponent as any, AppRootComponent as any,
@ -118,6 +120,7 @@ const PROVIDERS = [
DropZoneDirective, DropZoneDirective,
FastHtmlBindDirective, FastHtmlBindDirective,
AlwaysVisibleTypeaheadDirective, AlwaysVisibleTypeaheadDirective,
SortablejsModule,
], ],
}) })
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class

View File

@ -56,7 +56,7 @@ h3.mb-3 Profiles
*ngIf='group.editable && group.name', *ngIf='group.editable && group.name',
(click)='$event.stopPropagation(); deleteGroup(group)' (click)='$event.stopPropagation(); deleteGroup(group)'
) )
i.fas.fa-trash i.fas.fa-trash-alt
ng-container(*ngIf='!group.collapsed') ng-container(*ngIf='!group.collapsed')
ng-container(*ngFor='let profile of group.profiles') ng-container(*ngFor='let profile of group.profiles')
.list-group-item.pl-5.d-flex.align-items-center( .list-group-item.pl-5.d-flex.align-items-center(
@ -85,10 +85,10 @@ h3.mb-3 Profiles
button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); newProfile(profile)') button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); newProfile(profile)')
i.fas.fa-copy i.fas.fa-copy
button.btn.btn-link.text-danger.hover-reveal.ml-1( button.btn.btn-link.hover-reveal.ml-1(
*ngIf='!profile.isBuiltin', *ngIf='!profile.isBuiltin',
(click)='$event.stopPropagation(); deleteProfile(profile)' (click)='$event.stopPropagation(); deleteProfile(profile)'
) )
i.fas.fa-trash i.fas.fa-trash-alt
.ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}} .ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}}

View File

@ -189,6 +189,6 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
li(ngbNavItem) li(ngbNavItem)
a(ngbNavLink) Login scripts a(ngbNavLink) Login scripts
ng-template(ngbNavContent) ng-template(ngbNavContent)
login-scripts-settings([options]='profile.options') login-scripts-settings([options]='profile.options', #loginScriptsSettings)
div([ngbNavOutlet]='nav') div([ngbNavOutlet]='nav')

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core' import { Component, ViewChild } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core' import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core'
import { LoginScriptsSettingsComponent } from 'tabby-terminal'
import { PasswordStorageService } from '../services/passwordStorage.service' import { PasswordStorageService } from '../services/passwordStorage.service'
import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api' import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api'
import { SSHProfilesService } from '../profiles' import { SSHProfilesService } from '../profiles'
@ -20,6 +21,7 @@ export class SSHProfileSettingsComponent {
supportedAlgorithms: Record<string, string> = {} supportedAlgorithms: Record<string, string> = {}
algorithms: Record<string, Record<string, boolean>> = {} algorithms: Record<string, Record<string, boolean>> = {}
jumpHosts: SSHProfile[] jumpHosts: SSHProfile[]
@ViewChild('loginScriptsSettings') loginScriptsSettings: LoginScriptsSettingsComponent|null
constructor ( constructor (
public hostApp: HostAppService, public hostApp: HostAppService,
@ -92,6 +94,7 @@ export class SSHProfileSettingsComponent {
if (!this.useProxyCommand) { if (!this.useProxyCommand) {
this.profile.options.proxyCommand = undefined this.profile.options.proxyCommand = undefined
} }
this.loginScriptsSettings?.save()
} }
onForwardAdded (fw: ForwardedPortConfig) { onForwardAdded (fw: ForwardedPortConfig) {

View File

@ -24,17 +24,17 @@
"@types/deep-equal": "^1.0.0", "@types/deep-equal": "^1.0.0",
"@types/shell-escape": "^0.2.0", "@types/shell-escape": "^0.2.0",
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",
"binstring": "^0.2.1",
"buffer-replace": "^1.0.0",
"cli-spinner": "^0.2.10",
"dataurl": "0.1.0", "dataurl": "0.1.0",
"deep-equal": "2.0.5", "deep-equal": "2.0.5",
"hexer": "^1.5.0",
"ps-node": "^0.1.6", "ps-node": "^0.1.6",
"cli-spinner": "^0.2.10",
"runes": "^0.4.2", "runes": "^0.4.2",
"shell-escape": "^0.2.0", "shell-escape": "^0.2.0",
"utils-decorators": "^1.8.1", "utils-decorators": "^1.8.1",
"xterm": "^4.9.0-beta.7", "xterm": "^4.9.0-beta.7",
"binstring": "^0.2.1",
"buffer-replace": "^1.0.0",
"hexer": "^1.5.0",
"xterm-addon-fit": "^0.5.0", "xterm-addon-fit": "^0.5.0",
"xterm-addon-ligatures": "^0.5.0", "xterm-addon-ligatures": "^0.5.0",
"xterm-addon-search": "^0.8.0", "xterm-addon-search": "^0.8.0",

View File

@ -1,3 +1,4 @@
import deepClone from 'clone-deep'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { Logger } from 'tabby-core' import { Logger } from 'tabby-core'
@ -18,11 +19,28 @@ export class LoginScriptProcessor {
private outputToSession = new Subject<Buffer>() private outputToSession = new Subject<Buffer>()
private remainingScripts: LoginScript[] = [] private remainingScripts: LoginScript[] = []
private escapeSeqMap = {
a: '\x07',
b: '\x08',
e: '\x1b',
f: '\x0c',
n: '\x0a',
r: '\x0d',
t: '\x09',
v: '\x0b',
}
constructor ( constructor (
private logger: Logger, private logger: Logger,
options: LoginScriptsOptions options: LoginScriptsOptions
) { ) {
this.remainingScripts = options.scripts ?? [] this.remainingScripts = deepClone(options.scripts ?? [])
for (const script of this.remainingScripts) {
if (!script.isRegex) {
script.expect = this.unescape(script.expect)
}
script.send = this.unescape(script.send)
}
} }
feedFromSession (data: Buffer): boolean { feedFromSession (data: Buffer): boolean {
@ -34,25 +52,17 @@ export class LoginScriptProcessor {
continue continue
} }
let match = false let match = false
let cmd = ''
if (script.isRegex) { if (script.isRegex) {
const re = new RegExp(script.expect, 'g') const re = new RegExp(script.expect, 'g')
if (re.exec(dataString)) { match = re.test(dataString)
cmd = dataString.replace(re, script.send)
match = true
found = true
}
} else { } else {
if (dataString.includes(script.expect)) { match = dataString.includes(script.expect)
cmd = script.send
match = true
found = true
}
} }
if (match) { if (match) {
this.logger.info('Executing script: "' + cmd + '"') found = true
this.outputToSession.next(Buffer.from(cmd + '\n')) this.logger.info('Executing script:', script)
this.outputToSession.next(Buffer.from(script.send + '\n'))
this.remainingScripts = this.remainingScripts.filter(x => x !== script) this.remainingScripts = this.remainingScripts.filter(x => x !== script)
} else { } else {
if (script.optional) { if (script.optional) {
@ -83,4 +93,13 @@ export class LoginScriptProcessor {
} }
} }
} }
unescape (line: string): string {
line = line.replace(/\\((x\d{2})|(u\d{4}))/g, (match, g) => {
return String.fromCharCode(parseInt(g.substr(1), 16))
})
return line.replace(/\\(.)/g, (match, g) => {
return this.escapeSeqMap[g] || g
})
}
} }

View File

@ -1,37 +1,43 @@
table(*ngIf='options.scripts.length > 0') div([sortablejs]='scripts')
tr .input-group.flex-grow-1(*ngFor='let script of scripts')
th String to expect input.form-control(
th String to be sent type='text',
th.pl-2 Regex placeholder='Expect',
th.pl-2 Optional [(ngModel)]='script.expect'
th.pl-2 Actions )
tr(*ngFor='let script of options.scripts') input.form-control(
td.pr-2 type='text',
input.form-control( placeholder='Send',
type='text', [(ngModel)]='script.send'
[(ngModel)]='script.expect' )
) .input-group-append.input-group-text.p-0.border-0
td .hover-reveal(ngbDropdown)
input.form-control( button.btn.btn-link.btn-sm(ngbDropdownToggle)
type='text', i.fas.fa-fw.fa-cog
[(ngModel)]='script.send' .dropdown-menu-right(ngbDropdownMenu)
) a.dropdown-item(
td.pl-2 href='#',
checkbox( (click)='script.isRegex = false',
[(ngModel)]='script.isRegex', [class.active]='!script.isRegex',
) )
td.pl-2 i.fas.fa-fw([class.fa-check]='!script.isRegex')
checkbox( span Exact match
[(ngModel)]='script.optional', a.dropdown-item(
) href='#',
td.pl-2 (click)='script.isRegex = true',
.input-group.flex-nowrap [class.active]='script.isRegex',
button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)') )
i.fas.fa-arrow-up i.fas.fa-fw([class.fa-check]='script.isRegex')
button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)') span Regex
i.fas.fa-arrow-down a.dropdown-item(
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)') href='#',
i.fas.fa-trash (click)='script.optional = !script.optional',
[class.active]='script.optional',
)
i.fas.fa-fw([class.fa-check]='script.optional')
span Optional
button.btn.btn-link.btn-sm.hover-reveal((click)='deleteScript(script)')
i.fas.fa-fw.fa-trash-alt
button.btn.btn-outline-info.mt-2((click)='addScript()') button.btn.btn-outline-info.mt-2((click)='addScript()')
i.fas.fa-plus i.fas.fa-plus

View File

@ -11,29 +11,14 @@ import { LoginScript, LoginScriptsOptions } from '../api/loginScriptProcessing'
}) })
export class LoginScriptsSettingsComponent { export class LoginScriptsSettingsComponent {
@Input() options: LoginScriptsOptions @Input() options: LoginScriptsOptions
scripts: LoginScript[]
constructor ( constructor (
private platform: PlatformService, private platform: PlatformService,
) { } ) { }
ngOnInit () { ngOnInit () {
this.options.scripts ??= [] this.scripts = this.options.scripts ?? []
}
moveScriptUp (script: LoginScript) {
const index = this.options.scripts!.indexOf(script)
if (index > 0) {
this.options.scripts!.splice(index, 1)
this.options.scripts!.splice(index - 1, 0, script)
}
}
moveScriptDown (script: LoginScript) {
const index = this.options.scripts!.indexOf(script)
if (index >= 0 && index < this.options.scripts!.length - 1) {
this.options.scripts!.splice(index, 1)
this.options.scripts!.splice(index + 1, 0, script)
}
} }
async deleteScript (script: LoginScript) { async deleteScript (script: LoginScript) {
@ -46,11 +31,15 @@ export class LoginScriptsSettingsComponent {
defaultId: 1, defaultId: 1,
} }
)).response === 1) { )).response === 1) {
this.options.scripts = this.options.scripts!.filter(x => x !== script) this.scripts = this.scripts.filter(x => x !== script)
} }
} }
addScript () { addScript () {
this.options.scripts!.push({ expect: '', send: '' }) this.scripts.push({ expect: '', send: '' })
}
save () {
this.options.scripts = this.scripts
} }
} }

View File

@ -120,3 +120,4 @@ export * from './api/interfaces'
export * from './api/streamProcessing' export * from './api/streamProcessing'
export * from './api/loginScriptProcessing' export * from './api/loginScriptProcessing'
export * from './session' export * from './session'
export { LoginScriptsSettingsComponent, StreamProcessingSettingsComponent }

View File

@ -509,6 +509,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/sortablejs@^1.10.7":
version "1.10.7"
resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.10.7.tgz#ab9039c85429f0516955ec6dbc0bb20139417b15"
integrity sha512-lGCwwgpj8zW/ZmaueoPVSP7nnc9t8VqVWXS+ASX3eoUUENmiazv0rlXyTRludXzuX9ALjPsMqBu85TgJNWbTOg==
"@types/verror@^1.10.3": "@types/verror@^1.10.3":
version "1.10.4" version "1.10.4"
resolved "https://registry.npmjs.org/@types/verror/-/verror-1.10.4.tgz" resolved "https://registry.npmjs.org/@types/verror/-/verror-1.10.4.tgz"
@ -5036,6 +5041,13 @@ neo-async@^2.6.0, neo-async@^2.6.2:
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
ngx-sortablejs@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/ngx-sortablejs/-/ngx-sortablejs-11.1.0.tgz#14e50c48db908c1cb4b37722b28c2d3867c6140a"
integrity sha512-eM4dHwWSmXDcvF5gUmyMMQ0qqcqBXWCSZ9IRpqx4UkBKfo4N7pk/QuYh5io2fXVHWVFDaxW1yhn2FNpqxV6Jqw==
dependencies:
tslib "^2.0.0"
ngx-toastr@^14.0.0: ngx-toastr@^14.0.0:
version "14.0.0" version "14.0.0"
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-14.0.0.tgz#20e4737ef330b892a453768cd98b980558aeb286" resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-14.0.0.tgz#20e4737ef330b892a453768cd98b980558aeb286"
@ -7142,6 +7154,11 @@ socks@^1.1.10:
ip "^1.1.4" ip "^1.1.4"
smart-buffer "^1.0.13" smart-buffer "^1.0.13"
sortablejs@^1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
sorted-object@~2.0.1: sorted-object@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.npmjs.org/sorted-object/-/sorted-object-2.0.1.tgz" resolved "https://registry.npmjs.org/sorted-object/-/sorted-object-2.0.1.tgz"