diff --git a/terminus-ssh/package.json b/terminus-ssh/package.json index 5fbecdeb..91977e66 100644 --- a/terminus-ssh/package.json +++ b/terminus-ssh/package.json @@ -26,9 +26,11 @@ "ansi-colors": "^4.1.1", "cli-spinner": "^0.2.10", "run-script-os": "^1.1.3", - "ssh2": "^0.8.2", + "socksv5": "^0.0.6", + "ssh2": "^0.8.9", "ssh2-streams": "Eugeny/ssh2-streams#75f6d3425d071ac73a18fd46e2f5e738bfe897c5", "sshpk": "^1.16.1", + "strip-ansi": "^6.0.0", "temp": "^0.9.1", "terminus-terminal": "^1.0.98-nightly.0" }, diff --git a/terminus-ssh/src/api.ts b/terminus-ssh/src/api.ts index 4e9d662b..d94b69e3 100644 --- a/terminus-ssh/src/api.ts +++ b/terminus-ssh/src/api.ts @@ -1,4 +1,6 @@ import colors from 'ansi-colors' +import stripAnsi from 'strip-ansi' +import socksv5 from 'socksv5' import { BaseSession } from 'terminus-terminal' import { Server, Socket, createServer, createConnection } from 'net' import { Client, ClientChannel } from 'ssh2' @@ -43,7 +45,7 @@ export interface SSHConnection { } export enum PortForwardType { - Local, Remote + Local, Remote, Dynamic } export class ForwardedPort { @@ -55,13 +57,40 @@ export class ForwardedPort { private listener: Server - async startLocalListener (callback: (Socket) => void): Promise { - this.listener = createServer(callback) - return new Promise((resolve, reject) => { - this.listener.listen(this.port, '127.0.0.1') - this.listener.on('error', reject) - this.listener.on('listening', resolve) - }) + async startLocalListener (callback: (accept: () => Socket, reject: () => void, sourceAddress: string|null, sourcePort: number|null, targetAddress: string, targetPort: number) => void): Promise { + if (this.type === PortForwardType.Local) { + this.listener = createServer(s => callback( + () => s, + () => s.destroy(), + s.remoteAddress ?? null, + s.remotePort ?? null, + this.targetAddress, + this.targetPort, + )) + return new Promise((resolve, reject) => { + this.listener.listen(this.port, this.host) + this.listener.on('error', reject) + this.listener.on('listening', resolve) + }) + } else if (this.type === PortForwardType.Dynamic) { + return new Promise((resolve, reject) => { + this.listener = socksv5.createServer((info, accept, reject) => { + callback( + () => accept(true), + () => reject(), + null, + null, + info.dstAddr, + info.dstPort, + ) + }) + this.listener.on('error', reject) + this.listener.listen(this.port, this.host, resolve) + ;(this.listener as any).useAuth(socksv5.auth.None()) + }) + } else { + throw new Error('Invalid forward type for a local listener') + } } stopLocalListener (): void { @@ -71,8 +100,10 @@ export class ForwardedPort { toString (): string { if (this.type === PortForwardType.Local) { return `(local) ${this.host}:${this.port} → (remote) ${this.targetAddress}:${this.targetPort}` - } else { + } if (this.type === PortForwardType.Remote) { return `(remote) ${this.host}:${this.port} → (local) ${this.targetAddress}:${this.targetPort}` + } else { + return `(dynamic) ${this.host}:${this.port}` } } } @@ -232,25 +263,26 @@ export class SSHSession extends BaseSession { emitServiceMessage (msg: string): void { this.serviceMessage.next(msg) - this.logger.info(msg) + this.logger.info(stripAnsi(msg)) } async addPortForward (fw: ForwardedPort): Promise { - if (fw.type === PortForwardType.Local) { - await fw.startLocalListener((socket: Socket) => { + if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) { + await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => { this.logger.info(`New connection on ${fw}`) this.ssh.forwardOut( - socket.remoteAddress || '127.0.0.1', - socket.remotePort || 0, - fw.targetAddress, - fw.targetPort, + sourceAddress || '127.0.0.1', + sourcePort || 0, + targetAddress, + targetPort, (err, stream) => { if (err) { - this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection via ${fw}: ${err}`) - socket.destroy() + this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`) + reject() return } if (stream) { + const socket = accept() stream.pipe(socket) socket.pipe(stream) stream.on('close', () => { @@ -286,7 +318,7 @@ export class SSHSession extends BaseSession { } async removePortForward (fw: ForwardedPort): Promise { - if (fw.type === PortForwardType.Local) { + if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) { fw.stopLocalListener() this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw) } diff --git a/terminus-ssh/src/components/sshPortForwardingModal.component.pug b/terminus-ssh/src/components/sshPortForwardingModal.component.pug index f5afe02c..270ece9d 100644 --- a/terminus-ssh/src/components/sshPortForwardingModal.component.pug +++ b/terminus-ssh/src/components/sshPortForwardingModal.component.pug @@ -6,12 +6,22 @@ .list-group-item.d-flex.align-items-center(*ngFor='let fw of session.forwardedPorts') strong(*ngIf='fw.type === PortForwardType.Local') Local strong(*ngIf='fw.type === PortForwardType.Remote') Remote - .ml-3 {{fw.host}}:{{fw.port}} → {{fw.targetAddress}}:{{fw.targetPort}} + strong(*ngIf='fw.type === PortForwardType.Dynamic') Dynamic + .ml-3 {{fw.host}}:{{fw.port}} + .ml-2 → + .ml-2(*ngIf='fw.type !== PortForwardType.Dynamic') {{fw.targetAddress}}:{{fw.targetPort}} + .ml-2(*ngIf='fw.type === PortForwardType.Dynamic') SOCKS proxy button.btn.btn-link.ml-auto((click)='remove(fw)') i.fas.fa-trash-alt.mr-2 span Remove - .input-group.mb-2 + .input-group.mb-2(*ngIf='newForward.type === PortForwardType.Dynamic') + input.form-control(type='text', [(ngModel)]='newForward.host') + .input-group-append + .input-group-text : + input.form-control(type='number', [(ngModel)]='newForward.port') + + .input-group.mb-2(*ngIf='newForward.type !== PortForwardType.Dynamic') input.form-control(type='text', [(ngModel)]='newForward.host') .input-group-append .input-group-text : @@ -42,6 +52,13 @@ [value]='PortForwardType.Remote' ) | Remote + label.btn.btn-secondary.m-0(ngbButtonLabel) + input( + type='radio', + ngbButton, + [value]='PortForwardType.Dynamic' + ) + | Dynamic button.btn.btn-primary((click)='addForward()') i.fas.fa-check.mr-2 diff --git a/terminus-ssh/src/services/ssh.service.ts b/terminus-ssh/src/services/ssh.service.ts index ee8098c3..2be929ff 100644 --- a/terminus-ssh/src/services/ssh.service.ts +++ b/terminus-ssh/src/services/ssh.service.ts @@ -1,4 +1,5 @@ import colors from 'ansi-colors' +import stripAnsi from 'strip-ansi' import { open as openTemp } from 'temp' import { Injectable, NgZone } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' @@ -137,7 +138,7 @@ export class SSHService { const log = (s: any) => { logCallback!(s) - this.logger.info(s) + this.logger.info(stripAnsi(s)) } let privateKey: string|null = null diff --git a/terminus-ssh/webpack.config.js b/terminus-ssh/webpack.config.js index b0ef1451..482f99ee 100644 --- a/terminus-ssh/webpack.config.js +++ b/terminus-ssh/webpack.config.js @@ -50,6 +50,7 @@ module.exports = { 'keytar', 'path', 'ngx-toastr', + 'socksv5', 'windows-native-registry', 'windows-process-tree/build/Release/windows_process_tree.node', /^rxjs/, diff --git a/terminus-ssh/yarn.lock b/terminus-ssh/yarn.lock index c0387d7d..101f8e1f 100644 --- a/terminus-ssh/yarn.lock +++ b/terminus-ssh/yarn.lock @@ -32,6 +32,11 @@ ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + asn1@~0.2.0, asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -44,6 +49,11 @@ assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +async@0.2.x: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -69,11 +79,42 @@ cli-spinner@^0.2.10: resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47" integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q== +cli@0.4.x: + version "0.4.5" + resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.5.tgz#78f9485cd161b566e9a6c72d7170c4270e81db61" + integrity sha1-ePlIXNFhtWbppsctcXDEJw6B22E= + dependencies: + glob ">= 3.1.4" + +cliff@0.1.x: + version "0.1.10" + resolved "https://registry.yarnpkg.com/cliff/-/cliff-0.1.10.tgz#53be33ea9f59bec85609ee300ac4207603e52013" + integrity sha1-U74z6p9ZvshWCe4wCsQgdgPlIBM= + dependencies: + colors "~1.0.3" + eyes "~0.1.8" + winston "0.8.x" + +colors@0.6.x: + version "0.6.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc" + integrity sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w= + +colors@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI= + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -89,6 +130,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +eyes@0.1.x, eyes@~0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -101,7 +147,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob@^7.1.3: +"glob@>= 3.1.4", glob@^7.1.3: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -126,6 +172,20 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ipv6@*: + version "3.1.3" + resolved "https://registry.yarnpkg.com/ipv6/-/ipv6-3.1.3.tgz#4d9064f9c2dafa0dd10b8b7d76ffca4aad31b3b9" + integrity sha1-TZBk+cLa+g3RC4t9dv/KSq0xs7k= + dependencies: + cli "0.4.x" + cliff "0.1.x" + sprintf "0.1.x" + +isstream@0.1.x: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -150,6 +210,11 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= +pkginfo@0.3.x: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" + integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= + rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -167,6 +232,18 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +socksv5@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/socksv5/-/socksv5-0.0.6.tgz#1327235ff7e8de21ac434a0a579dc69c3f071061" + integrity sha1-EycjX/fo3iGsQ0oKV53GnD8HEGE= + dependencies: + ipv6 "*" + +sprintf@0.1.x: + version "0.1.5" + resolved "https://registry.yarnpkg.com/sprintf/-/sprintf-0.1.5.tgz#8f83e39a9317c1a502cb7db8050e51c679f6edcf" + integrity sha1-j4PjmpMXwaUCy324BQ5Rxnn27c8= + ssh2-streams@Eugeny/ssh2-streams#75f6d3425d071ac73a18fd46e2f5e738bfe897c5: version "0.4.10" resolved "https://codeload.github.com/Eugeny/ssh2-streams/tar.gz/75f6d3425d071ac73a18fd46e2f5e738bfe897c5" @@ -184,7 +261,7 @@ ssh2-streams@~0.4.10: bcrypt-pbkdf "^1.0.2" streamsearch "~0.1.2" -ssh2@^0.8.2: +ssh2@^0.8.9: version "0.8.9" resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3" integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw== @@ -206,11 +283,23 @@ sshpk@^1.16.1: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + streamsearch@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + temp@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.1.tgz#2d666114fafa26966cd4065996d7ceedd4dd4697" @@ -228,6 +317,19 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +winston@0.8.x: + version "0.8.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-0.8.3.tgz#64b6abf4cd01adcaefd5009393b1d8e8bec19db0" + integrity sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA= + dependencies: + async "0.2.x" + colors "0.6.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + pkginfo "0.3.x" + stack-trace "0.0.x" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"