mirror of
https://github.com/Eugeny/tabby.git
synced 2024-12-24 02:53:43 +03:00
added SSH connection manager (fixes #220)
This commit is contained in:
parent
13a76db9af
commit
5cdb7527c8
@ -31,10 +31,6 @@
|
|||||||
"rxjs": "5.3.0",
|
"rxjs": "5.3.0",
|
||||||
"zone.js": "0.8.12"
|
"zone.js": "0.8.12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
|
||||||
"wincredmgr": "^2.0.0",
|
|
||||||
"xkeychain": "^0.0.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mz": "0.0.31"
|
"@types/mz": "0.0.31"
|
||||||
}
|
}
|
||||||
|
@ -268,14 +268,6 @@ util@^0.10.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
inherits "2.0.1"
|
inherits "2.0.1"
|
||||||
|
|
||||||
wincredmgr@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/wincredmgr/-/wincredmgr-2.0.0.tgz#cab8e3c6d98f0ea255d7638e82fd9f393ca7f515"
|
|
||||||
|
|
||||||
xkeychain@^0.0.6:
|
|
||||||
version "0.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/xkeychain/-/xkeychain-0.0.6.tgz#1c58b3dd2f80481f8f67949c3511aa14027c2b9b"
|
|
||||||
|
|
||||||
zone.js@0.8.12:
|
zone.js@0.8.12:
|
||||||
version "0.8.12"
|
version "0.8.12"
|
||||||
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.12.tgz#86ff5053c98aec291a0bf4bbac501d694a05cfbb"
|
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.12.tgz#86ff5053c98aec291a0bf4bbac501d694a05cfbb"
|
||||||
|
@ -3,5 +3,5 @@ const rebuild = require('electron-rebuild').default
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const vars = require('./vars')
|
const vars = require('./vars')
|
||||||
|
|
||||||
rebuild(path.resolve(__dirname, '../app'), vars.electronVersion, process.arch, [], true)
|
rebuild(path.resolve(__dirname, '../terminus-ssh'), vars.electronVersion, process.arch, [], true)
|
||||||
rebuild(path.resolve(__dirname, '../terminus-terminal'), vars.electronVersion, process.arch, [], true)
|
rebuild(path.resolve(__dirname, '../terminus-terminal'), vars.electronVersion, process.arch, [], true)
|
||||||
|
@ -14,6 +14,7 @@ exports.builtinPlugins = [
|
|||||||
'terminus-terminal',
|
'terminus-terminal',
|
||||||
'terminus-community-color-schemes',
|
'terminus-community-color-schemes',
|
||||||
'terminus-plugin-manager',
|
'terminus-plugin-manager',
|
||||||
|
'terminus-ssh',
|
||||||
]
|
]
|
||||||
exports.nativeModules = ['node-pty-tmp', 'font-manager', 'xkeychain']
|
exports.nativeModules = ['node-pty-tmp', 'font-manager', 'xkeychain']
|
||||||
exports.electronVersion = pkgInfo.devDependencies.electron
|
exports.electronVersion = pkgInfo.devDependencies.electron
|
||||||
|
@ -43,3 +43,4 @@ hotkeys:
|
|||||||
tab-10:
|
tab-10:
|
||||||
- 'Alt-0'
|
- 'Alt-0'
|
||||||
- ['Ctrl-A', '0']
|
- ['Ctrl-A', '0']
|
||||||
|
pluginBlacklist: ['ssh']
|
||||||
|
@ -43,3 +43,4 @@ hotkeys:
|
|||||||
tab-10:
|
tab-10:
|
||||||
- '⌘-0'
|
- '⌘-0'
|
||||||
- ['Ctrl-A', '0']
|
- ['Ctrl-A', '0']
|
||||||
|
pluginBlacklist: ['ssh']
|
||||||
|
@ -43,3 +43,4 @@ hotkeys:
|
|||||||
tab-10:
|
tab-10:
|
||||||
- 'Alt-0'
|
- 'Alt-0'
|
||||||
- ['Ctrl-A', '0']
|
- ['Ctrl-A', '0']
|
||||||
|
pluginBlacklist: []
|
||||||
|
@ -6,4 +6,3 @@ appearance:
|
|||||||
theme: Standard
|
theme: Standard
|
||||||
frame: thin
|
frame: thin
|
||||||
css: '/* * { color: blue !important; } */'
|
css: '/* * { color: blue !important; } */'
|
||||||
pluginBlacklist: []
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { SettingsTabProvider, ComponentType } from 'terminus-settings'
|
import { SettingsTabProvider } from 'terminus-settings'
|
||||||
|
|
||||||
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ export class PluginsSettingsTabProvider extends SettingsTabProvider {
|
|||||||
id = 'plugins'
|
id = 'plugins'
|
||||||
title = 'Plugins'
|
title = 'Plugins'
|
||||||
|
|
||||||
getComponentType (): ComponentType {
|
getComponentType (): any {
|
||||||
return PluginsSettingsTabComponent
|
return PluginsSettingsTabComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
export declare type ComponentType = new (...args: any[]) => any
|
|
||||||
|
|
||||||
export abstract class SettingsTabProvider {
|
export abstract class SettingsTabProvider {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
getComponentType (): ComponentType {
|
getComponentType (): any {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
terminus-ssh/package.json
Normal file
48
terminus-ssh/package.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "terminus-ssh",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "SSH connection manager for Terminus",
|
||||||
|
"keywords": [
|
||||||
|
"terminus-builtin-plugin"
|
||||||
|
],
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --progress --color",
|
||||||
|
"watch": "webpack --progress --color --watch"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"author": "Eugene Pankov",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ssh2": "^0.5.35",
|
||||||
|
"@types/webpack-env": "^1.13.0",
|
||||||
|
"apply-loader": "^2.0.0",
|
||||||
|
"awesome-typescript-loader": "^3.1.2",
|
||||||
|
"electron": "^1.6.11",
|
||||||
|
"pug": "^2.0.0-rc.3",
|
||||||
|
"pug-loader": "^2.3.0",
|
||||||
|
"rxjs": "^5.4.0",
|
||||||
|
"typescript": "^2.2.2",
|
||||||
|
"webpack": "^2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "^4.1.3",
|
||||||
|
"@angular/core": "^4.1.3",
|
||||||
|
"@angular/forms": "^4.1.3",
|
||||||
|
"@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.29",
|
||||||
|
"terminus-core": "*",
|
||||||
|
"terminus-settings": "*",
|
||||||
|
"terminus-terminal": "*"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"wincredmgr": "^2.0.0",
|
||||||
|
"xkeychain": "^0.0.6"
|
||||||
|
},
|
||||||
|
"repository": "eugeny/terminus-ssh",
|
||||||
|
"dependencies": {
|
||||||
|
"ssh2": "^0.5.5"
|
||||||
|
}
|
||||||
|
}
|
51
terminus-ssh/src/api.ts
Normal file
51
terminus-ssh/src/api.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { BaseSession } from 'terminus-terminal'
|
||||||
|
|
||||||
|
export interface SSHConnection {
|
||||||
|
name?: string
|
||||||
|
host: string
|
||||||
|
user: string
|
||||||
|
password?: string
|
||||||
|
privateKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SSHSession extends BaseSession {
|
||||||
|
constructor (private shell: any) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.open = true
|
||||||
|
|
||||||
|
this.shell.on('data', data => {
|
||||||
|
this.emitOutput(data.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
this.shell.on('end', () => {
|
||||||
|
if (this.open) {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resize (columns, rows) {
|
||||||
|
this.shell.setWindow(rows, columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
write (data) {
|
||||||
|
this.shell.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
kill (signal?: string) {
|
||||||
|
this.shell.signal(signal || 'TERM')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChildProcesses (): Promise<any[]> {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
async gracefullyKillProcess (): Promise<void> {
|
||||||
|
this.kill('TERM')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkingDirectory (): Promise<string> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
37
terminus-ssh/src/buttonProvider.ts
Normal file
37
terminus-ssh/src/buttonProvider.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { HotkeysService, ToolbarButtonProvider, IToolbarButton } from 'terminus-core'
|
||||||
|
import { SSHModalComponent } from './components/sshModal.component'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ButtonProvider extends ToolbarButtonProvider {
|
||||||
|
constructor (
|
||||||
|
private ngbModal: NgbModal,
|
||||||
|
hotkeys: HotkeysService,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
|
||||||
|
if (hotkey === 'ssh') {
|
||||||
|
this.activate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
activate () {
|
||||||
|
let modal = this.ngbModal.open(SSHModalComponent)
|
||||||
|
modal.result.then(() => {
|
||||||
|
//this.terminal.openTab(shell)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
provide (): IToolbarButton[] {
|
||||||
|
return [{
|
||||||
|
icon: 'globe',
|
||||||
|
weight: 5,
|
||||||
|
title: 'SSH connections',
|
||||||
|
click: async () => {
|
||||||
|
this.activate()
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
.modal-body
|
||||||
|
.form-group
|
||||||
|
label Name
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[(ngModel)]='connection.name',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label Host
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[(ngModel)]='connection.host',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label Username
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[(ngModel)]='connection.user',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label Private key
|
||||||
|
.input-group
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
placeholder='Key file path',
|
||||||
|
[(ngModel)]='connection.privateKey'
|
||||||
|
)
|
||||||
|
.input-group-btn
|
||||||
|
button.btn.btn-secondary((click)='selectPrivateKey()')
|
||||||
|
i.fa.fa-folder-open
|
||||||
|
|
||||||
|
.modal-footer
|
||||||
|
button.btn.btn-outline-primary((click)='save()') Save
|
||||||
|
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
38
terminus-ssh/src/components/editConnectionModal.component.ts
Normal file
38
terminus-ssh/src/components/editConnectionModal.component.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ElectronService, HostAppService } from 'terminus-core'
|
||||||
|
import { SSHConnection } from '../api'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: require('./editConnectionModal.component.pug'),
|
||||||
|
})
|
||||||
|
export class EditConnectionModalComponent {
|
||||||
|
connection: SSHConnection
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private modalInstance: NgbActiveModal,
|
||||||
|
private electron: ElectronService,
|
||||||
|
private hostApp: HostAppService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
selectPrivateKey () {
|
||||||
|
let path = this.electron.dialog.showOpenDialog(
|
||||||
|
this.hostApp.getWindow(),
|
||||||
|
{
|
||||||
|
title: 'Select private key',
|
||||||
|
properties: ['openDirectory']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (path) {
|
||||||
|
this.connection.privateKey = path[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.modalInstance.close(this.connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel () {
|
||||||
|
this.modalInstance.dismiss()
|
||||||
|
}
|
||||||
|
}
|
9
terminus-ssh/src/components/promptModal.component.pug
Normal file
9
terminus-ssh/src/components/promptModal.component.pug
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.modal-body
|
||||||
|
input.form-control(
|
||||||
|
[type]='password ? "password" : "text"',
|
||||||
|
[(ngModel)]='value',
|
||||||
|
#input,
|
||||||
|
[placeholder]='prompt',
|
||||||
|
(keyup.enter)='ok()',
|
||||||
|
(keyup.esc)='cancel()',
|
||||||
|
)
|
27
terminus-ssh/src/components/promptModal.component.ts
Normal file
27
terminus-ssh/src/components/promptModal.component.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Component, Input, ViewChild, ElementRef } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: require('./promptModal.component.pug'),
|
||||||
|
})
|
||||||
|
export class PromptModalComponent {
|
||||||
|
@Input() value: string
|
||||||
|
@Input() password: boolean
|
||||||
|
@ViewChild('input') input: ElementRef
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private modalInstance: NgbActiveModal,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.input.nativeElement.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
ok () {
|
||||||
|
this.modalInstance.close(this.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel () {
|
||||||
|
this.modalInstance.close('')
|
||||||
|
}
|
||||||
|
}
|
24
terminus-ssh/src/components/sshModal.component.pug
Normal file
24
terminus-ssh/src/components/sshModal.component.pug
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.modal-body
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[(ngModel)]='quickTarget',
|
||||||
|
autofocus,
|
||||||
|
placeholder='Quick connect: [user@]host',
|
||||||
|
(keyup.enter)='quickConnect()'
|
||||||
|
)
|
||||||
|
|
||||||
|
.list-group.mt-3(*ngIf='lastConnection')
|
||||||
|
a.list-group-item.list-group-item-action((click)='connect(lastConnection)')
|
||||||
|
i.fa.fa-fw.fa-history
|
||||||
|
span {{lastConnection.name}}
|
||||||
|
|
||||||
|
.list-group.mt-3
|
||||||
|
a.list-group-item.list-group-item-action(*ngFor='let connection of connections', (click)='connect(connection)')
|
||||||
|
i.fa.fa-fw.fa-globe
|
||||||
|
span {{connection.name}}
|
||||||
|
a.list-group-item.list-group-item-action((click)='manageConnections()')
|
||||||
|
i.fa.fa-fw.fa-wrench
|
||||||
|
span Manage connections
|
||||||
|
|
||||||
|
//.modal-footer
|
||||||
|
button.btn.btn-outline-primary((click)='close()') Cancel
|
60
terminus-ssh/src/components/sshModal.component.ts
Normal file
60
terminus-ssh/src/components/sshModal.component.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ConfigService, AppService } from 'terminus-core'
|
||||||
|
import { SettingsTabComponent } from 'terminus-settings'
|
||||||
|
import { SSHService } from '../services/ssh.service'
|
||||||
|
import { SSHConnection } from '../api'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: require('./sshModal.component.pug'),
|
||||||
|
//styles: [require('./sshModal.component.scss')],
|
||||||
|
})
|
||||||
|
export class SSHModalComponent {
|
||||||
|
connections: SSHConnection[]
|
||||||
|
quickTarget: string
|
||||||
|
lastConnection: SSHConnection
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public modalInstance: NgbActiveModal,
|
||||||
|
private config: ConfigService,
|
||||||
|
private ssh: SSHService,
|
||||||
|
private app: AppService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.connections = this.config.store.ssh.connections
|
||||||
|
if (window.localStorage.lastConnection) {
|
||||||
|
this.lastConnection = JSON.parse(window.localStorage.lastConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quickConnect () {
|
||||||
|
let user = 'root'
|
||||||
|
let host = this.quickTarget
|
||||||
|
if (host.includes('@')) {
|
||||||
|
[user, host] = host.split('@')
|
||||||
|
}
|
||||||
|
let connection: SSHConnection = {
|
||||||
|
name: this.quickTarget,
|
||||||
|
host, user,
|
||||||
|
}
|
||||||
|
window.localStorage.lastConnection = JSON.stringify(connection)
|
||||||
|
this.connect(connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
connect (connection: SSHConnection) {
|
||||||
|
this.close()
|
||||||
|
this.ssh.connect(connection).catch(error => {
|
||||||
|
alert(`Could not connect: ${error}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
manageConnections () {
|
||||||
|
this.close()
|
||||||
|
this.app.openNewTab(SettingsTabComponent, { activeTab: 'ssh' })
|
||||||
|
}
|
||||||
|
|
||||||
|
close () {
|
||||||
|
this.modalInstance.close()
|
||||||
|
}
|
||||||
|
}
|
15
terminus-ssh/src/components/sshSettingsTab.component.pug
Normal file
15
terminus-ssh/src/components/sshSettingsTab.component.pug
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
h3 Connections
|
||||||
|
|
||||||
|
.list-group.mt-3.mb-3
|
||||||
|
.list-group-item(*ngFor='let connection of connections')
|
||||||
|
.d-flex.w-100
|
||||||
|
.mr-auto
|
||||||
|
div
|
||||||
|
span {{connection.name}}
|
||||||
|
.text-muted {{connection.host}}
|
||||||
|
button.btn.btn-outline-info.ml-2((click)='editConnection(connection)')
|
||||||
|
i.fa.fa-pencil
|
||||||
|
button.btn.btn-outline-danger.ml-1((click)='deleteConnection(connection)')
|
||||||
|
i.fa.fa-trash-o
|
||||||
|
|
||||||
|
button.btn.btn-outline-primary((click)='createConnection()') Add connection
|
52
terminus-ssh/src/components/sshSettingsTab.component.ts
Normal file
52
terminus-ssh/src/components/sshSettingsTab.component.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ConfigService } from 'terminus-core'
|
||||||
|
import { SSHConnection } from '../api'
|
||||||
|
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: require('./sshSettingsTab.component.pug'),
|
||||||
|
})
|
||||||
|
export class SSHSettingsTabComponent {
|
||||||
|
connections: SSHConnection[]
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public config: ConfigService,
|
||||||
|
private ngbModal: NgbModal,
|
||||||
|
) {
|
||||||
|
this.connections = this.config.store.ssh.connections
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit () {
|
||||||
|
}
|
||||||
|
|
||||||
|
createConnection () {
|
||||||
|
let connection: SSHConnection = {
|
||||||
|
name: '',
|
||||||
|
host: '',
|
||||||
|
user: 'root',
|
||||||
|
}
|
||||||
|
let modal = this.ngbModal.open(EditConnectionModalComponent)
|
||||||
|
modal.componentInstance.connection = connection
|
||||||
|
modal.result.then(result => {
|
||||||
|
this.connections.push(result)
|
||||||
|
this.config.store.ssh.connections = this.connections
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
editConnection (connection: SSHConnection) {
|
||||||
|
let modal = this.ngbModal.open(EditConnectionModalComponent)
|
||||||
|
modal.componentInstance.connection = Object.assign({}, connection)
|
||||||
|
modal.result.then(result => {
|
||||||
|
Object.assign(connection, result)
|
||||||
|
this.config.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteConnection (connection: SSHConnection) {
|
||||||
|
if (confirm(`Delete "${connection.name}"?`)) {
|
||||||
|
this.connections = this.connections.filter(x => x !== connection)
|
||||||
|
this.config.store.ssh.connections = this.connections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
terminus-ssh/src/config.ts
Normal file
18
terminus-ssh/src/config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ConfigProvider } from 'terminus-core'
|
||||||
|
|
||||||
|
export class SSHConfigProvider extends ConfigProvider {
|
||||||
|
defaults = {
|
||||||
|
ssh: {
|
||||||
|
connections: [],
|
||||||
|
options: {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hotkeys: {
|
||||||
|
'ssh': [
|
||||||
|
'Alt-S',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
platformDefaults = { }
|
||||||
|
}
|
43
terminus-ssh/src/index.ts
Normal file
43
terminus-ssh/src/index.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ToolbarButtonProvider, ConfigProvider } from 'terminus-core'
|
||||||
|
import { SettingsTabProvider } from 'terminus-settings'
|
||||||
|
|
||||||
|
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
|
||||||
|
import { SSHModalComponent } from './components/sshModal.component'
|
||||||
|
import { PromptModalComponent } from './components/promptModal.component'
|
||||||
|
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||||
|
import { SSHService } from './services/ssh.service'
|
||||||
|
|
||||||
|
import { ButtonProvider } from './buttonProvider'
|
||||||
|
import { SSHConfigProvider } from './config'
|
||||||
|
import { SSHSettingsTabProvider } from './settings'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
NgbModule,
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
SSHService,
|
||||||
|
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||||
|
{ provide: ConfigProvider, useClass: SSHConfigProvider, multi: true },
|
||||||
|
{ provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true },
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
EditConnectionModalComponent,
|
||||||
|
PromptModalComponent,
|
||||||
|
SSHModalComponent,
|
||||||
|
SSHSettingsTabComponent,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
EditConnectionModalComponent,
|
||||||
|
PromptModalComponent,
|
||||||
|
SSHModalComponent,
|
||||||
|
SSHSettingsTabComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export default class SSHModule { }
|
128
terminus-ssh/src/services/ssh.service.ts
Normal file
128
terminus-ssh/src/services/ssh.service.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { Client } from 'ssh2'
|
||||||
|
import * as fs from 'mz/fs'
|
||||||
|
import { AppService } from 'terminus-core'
|
||||||
|
import { TerminalTabComponent } from 'terminus-terminal'
|
||||||
|
import { SSHConnection, SSHSession } from '../api'
|
||||||
|
import { PromptModalComponent } from '../components/promptModal.component'
|
||||||
|
|
||||||
|
const { SSH2Stream } = require('ssh2-streams')
|
||||||
|
const keychain = require('xkeychain')
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SSHService {
|
||||||
|
constructor (
|
||||||
|
private app: AppService,
|
||||||
|
private zone: NgZone,
|
||||||
|
private ngbModal: NgbModal,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect (connection: SSHConnection): Promise<TerminalTabComponent> {
|
||||||
|
let privateKey: string = null
|
||||||
|
if (connection.privateKey) {
|
||||||
|
try {
|
||||||
|
privateKey = (await fs.readFile(connection.privateKey)).toString()
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ssh = new Client()
|
||||||
|
let connected = false
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
ssh.on('ready', () => {
|
||||||
|
connected = true
|
||||||
|
this.zone.run(resolve)
|
||||||
|
})
|
||||||
|
ssh.on('error', error => {
|
||||||
|
this.zone.run(() => {
|
||||||
|
if (connected) {
|
||||||
|
alert(`SSH error: ${error}`)
|
||||||
|
} else {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
|
||||||
|
console.log(name, instructions, instructionsLang)
|
||||||
|
let results = []
|
||||||
|
for (let prompt of prompts) {
|
||||||
|
let modal = this.ngbModal.open(PromptModalComponent)
|
||||||
|
modal.componentInstance.prompt = prompt.prompt
|
||||||
|
modal.componentInstance.password = !prompt.echo
|
||||||
|
results.push(await modal.result)
|
||||||
|
}
|
||||||
|
finish(results)
|
||||||
|
}))
|
||||||
|
ssh.connect({
|
||||||
|
host: connection.host,
|
||||||
|
username: connection.user,
|
||||||
|
password: privateKey ? undefined : '',
|
||||||
|
privateKey,
|
||||||
|
tryKeyboard: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
let keychainPasswordUsed = false
|
||||||
|
|
||||||
|
;(ssh as any).config.password = () => this.zone.run(async () => {
|
||||||
|
if (connection.password) {
|
||||||
|
return connection.password
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keychainPasswordUsed && keychain.isSupported()) {
|
||||||
|
let password = await new Promise(resolve => {
|
||||||
|
keychain.getPassword({
|
||||||
|
account: connection.user,
|
||||||
|
service: `ssh@${connection.host}`,
|
||||||
|
}, (_, result) => resolve(result))
|
||||||
|
})
|
||||||
|
if (password) {
|
||||||
|
keychainPasswordUsed = true
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let modal = this.ngbModal.open(PromptModalComponent)
|
||||||
|
modal.componentInstance.prompt = `Password for ${connection.user}@${connection.host}`
|
||||||
|
modal.componentInstance.password = true
|
||||||
|
let password = await modal.result
|
||||||
|
|
||||||
|
keychain.setPassword({
|
||||||
|
account: connection.user,
|
||||||
|
service: `ssh@${connection.host}`,
|
||||||
|
password
|
||||||
|
}, () => null)
|
||||||
|
|
||||||
|
return password
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
let shell = await new Promise((resolve, reject) => {
|
||||||
|
ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(shell)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let session = new SSHSession(shell)
|
||||||
|
|
||||||
|
return this.zone.run(() => this.app.openNewTab(
|
||||||
|
TerminalTabComponent,
|
||||||
|
{ session, sessionOptions: {} }
|
||||||
|
) as TerminalTabComponent)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _authPassword = SSH2Stream.prototype.authPassword
|
||||||
|
SSH2Stream.prototype.authPassword = async function (username, passwordFn) {
|
||||||
|
_authPassword.bind(this)(username, await passwordFn())
|
||||||
|
}
|
14
terminus-ssh/src/settings.ts
Normal file
14
terminus-ssh/src/settings.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { SettingsTabProvider } from 'terminus-settings'
|
||||||
|
|
||||||
|
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SSHSettingsTabProvider extends SettingsTabProvider {
|
||||||
|
id = 'ssh'
|
||||||
|
title = 'SSH'
|
||||||
|
|
||||||
|
getComponentType (): any {
|
||||||
|
return SSHSettingsTabComponent
|
||||||
|
}
|
||||||
|
}
|
8
terminus-ssh/tsconfig.json
Normal file
8
terminus-ssh/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
|
"declarationDir": "dist"
|
||||||
|
}
|
||||||
|
}
|
46
terminus-ssh/webpack.config.js
Normal file
46
terminus-ssh/webpack.config.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
target: 'node',
|
||||||
|
entry: 'src/index.ts',
|
||||||
|
devtool: 'source-map',
|
||||||
|
context: __dirname,
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'index.js',
|
||||||
|
pathinfo: true,
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
devtoolModuleFilenameTemplate: 'webpack-terminus-ssh:///[resource-path]',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: ['.', 'src', 'node_modules', '../app/node_modules'].map(x => path.join(__dirname, x)),
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
loader: 'awesome-typescript-loader',
|
||||||
|
query: {
|
||||||
|
configFileName: path.resolve(__dirname, 'tsconfig.json'),
|
||||||
|
typeRoots: [path.resolve(__dirname, 'node_modules/@types')],
|
||||||
|
paths: {
|
||||||
|
"terminus-*": [path.resolve(__dirname, '../terminus-*')],
|
||||||
|
"*": [path.resolve(__dirname, '../app/node_modules/*')],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
|
||||||
|
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
'fs',
|
||||||
|
'node-ssh',
|
||||||
|
'xkeychain',
|
||||||
|
/^rxjs/,
|
||||||
|
/^@angular/,
|
||||||
|
/^@ng-bootstrap/,
|
||||||
|
/^terminus-/,
|
||||||
|
]
|
||||||
|
}
|
2968
terminus-ssh/yarn.lock
Normal file
2968
terminus-ssh/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { SettingsTabProvider, ComponentType } from 'terminus-settings'
|
import { SettingsTabProvider } from 'terminus-settings'
|
||||||
|
|
||||||
import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.component'
|
import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.component'
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ export class TerminalSettingsTabProvider extends SettingsTabProvider {
|
|||||||
id = 'terminal'
|
id = 'terminal'
|
||||||
title = 'Terminal'
|
title = 'Terminal'
|
||||||
|
|
||||||
getComponentType (): ComponentType {
|
getComponentType (): any {
|
||||||
return TerminalSettingsTabComponent
|
return TerminalSettingsTabComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,5 @@ module.exports = [
|
|||||||
require('./terminus-terminal/webpack.config.js'),
|
require('./terminus-terminal/webpack.config.js'),
|
||||||
require('./terminus-community-color-schemes/webpack.config.js'),
|
require('./terminus-community-color-schemes/webpack.config.js'),
|
||||||
require('./terminus-plugin-manager/webpack.config.js'),
|
require('./terminus-plugin-manager/webpack.config.js'),
|
||||||
|
require('./terminus-ssh/webpack.config.js'),
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user