mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-05 02:50:00 +00:00
wip
This commit is contained in:
parent
d265b5f8ab
commit
c8d5b7ab61
@ -125,7 +125,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!session.open) {
|
if (!session.open) {
|
||||||
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` Connecting to ${session.profile.options.host}\r\n`)
|
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` Connecting to ${session.profile.name}\r\n`)
|
||||||
|
|
||||||
this.startSpinner(this.translate.instant(_('Connecting')))
|
this.startSpinner(this.translate.instant(_('Connecting')))
|
||||||
|
|
||||||
|
@ -53,14 +53,6 @@ export class SSHShellSession extends BaseSession {
|
|||||||
|
|
||||||
this.loginScriptProcessor?.executeUnconditionalScripts()
|
this.loginScriptProcessor?.executeUnconditionalScripts()
|
||||||
|
|
||||||
// this.shell.on('greeting', greeting => {
|
|
||||||
// this.emitServiceMessage(`Shell greeting: ${greeting}`)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// this.shell.on('banner', banner => {
|
|
||||||
// this.emitServiceMessage(`Shell banner: ${banner}`)
|
|
||||||
// })
|
|
||||||
|
|
||||||
this.shell.data$.subscribe(data => {
|
this.shell.data$.subscribe(data => {
|
||||||
this.emitOutput(Buffer.from(data))
|
this.emitOutput(Buffer.from(data))
|
||||||
})
|
})
|
||||||
|
@ -3,10 +3,11 @@ import * as crypto from 'crypto'
|
|||||||
import * as sshpk from 'sshpk'
|
import * as sshpk from 'sshpk'
|
||||||
import colors from 'ansi-colors'
|
import colors from 'ansi-colors'
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
|
import * as shellQuote from 'shell-quote'
|
||||||
import { Injector } from '@angular/core'
|
import { Injector } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, PromptModalComponent, LogService, Logger, TranslateService } from 'tabby-core'
|
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, PromptModalComponent, LogService, Logger, TranslateService } from 'tabby-core'
|
||||||
// import { Socket } from 'net'
|
import { Socket } from 'net'
|
||||||
// import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
// import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { HostKeyPromptModalComponent } from '../components/hostKeyPromptModal.component'
|
import { HostKeyPromptModalComponent } from '../components/hostKeyPromptModal.component'
|
||||||
@ -14,7 +15,7 @@ import { HostKeyPromptModalComponent } from '../components/hostKeyPromptModal.co
|
|||||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||||
import { SSHKnownHostsService } from '../services/sshKnownHosts.service'
|
import { SSHKnownHostsService } from '../services/sshKnownHosts.service'
|
||||||
import { SFTPSession } from './sftp'
|
import { SFTPSession } from './sftp'
|
||||||
import { SSHAlgorithmType, SSHProfile, SSHProxyStream, AutoPrivateKeyLocator } from '../api'
|
import { SSHAlgorithmType, SSHProfile, SSHProxyStream, AutoPrivateKeyLocator, PortForwardType } from '../api'
|
||||||
import { ForwardedPort } from './forwards'
|
import { ForwardedPort } from './forwards'
|
||||||
import { X11Socket } from './x11'
|
import { X11Socket } from './x11'
|
||||||
import { supportedAlgorithms } from '../algorithms'
|
import { supportedAlgorithms } from '../algorithms'
|
||||||
@ -33,11 +34,6 @@ interface AuthMethod {
|
|||||||
contents?: Buffer
|
contents?: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface Handshake {
|
|
||||||
// kex: string
|
|
||||||
// serverHostKey: string
|
|
||||||
// }
|
|
||||||
|
|
||||||
export class KeyboardInteractivePrompt {
|
export class KeyboardInteractivePrompt {
|
||||||
readonly responses: string[] = []
|
readonly responses: string[] = []
|
||||||
|
|
||||||
@ -200,7 +196,6 @@ export class SSHSession {
|
|||||||
// return new SFTPSession(this.sftp, this.injector)
|
// return new SFTPSession(this.sftp, this.injector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async start (): Promise<void> {
|
async start (): Promise<void> {
|
||||||
// const log = (s: any) => this.emitServiceMessage(s)
|
// const log = (s: any) => this.emitServiceMessage(s)
|
||||||
|
|
||||||
@ -212,8 +207,30 @@ export class SSHSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// todo migrate connection opts
|
// todo migrate connection opts
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/init-declarations
|
||||||
|
let transport: russh.SshTransport
|
||||||
|
if (this.profile.options.proxyCommand) {
|
||||||
|
this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
|
||||||
|
|
||||||
|
const argv = shellQuote.parse(this.profile.options.proxyCommand)
|
||||||
|
transport = await russh.SshTransport.newCommand(argv[0], argv.slice(1))
|
||||||
|
// TODO stderr service messages
|
||||||
|
// this.proxyCommandStream.destroyed$.subscribe(err => {
|
||||||
|
// if (err) {
|
||||||
|
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
||||||
|
// this.destroy()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// this.proxyCommandStream.message$.subscribe(message => {
|
||||||
|
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ' ' + message.trim())
|
||||||
|
// })
|
||||||
|
} else {
|
||||||
|
transport = await russh.SshTransport.newSocket(`${this.profile.options.host.trim()}:${this.profile.options.port ?? 22}`)
|
||||||
|
}
|
||||||
|
|
||||||
this.ssh = await russh.SSHClient.connect(
|
this.ssh = await russh.SSHClient.connect(
|
||||||
`${this.profile.options.host.trim()}:${this.profile.options.port ?? 22}`,
|
transport,
|
||||||
async key => {
|
async key => {
|
||||||
if (!await this.verifyHostKey(key)) {
|
if (!await this.verifyHostKey(key)) {
|
||||||
return false
|
return false
|
||||||
@ -231,13 +248,19 @@ export class SSHSession {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.ssh.banner$.subscribe(banner => {
|
||||||
|
if (!this.profile.options.skipBanner) {
|
||||||
|
this.emitServiceMessage(banner)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.ssh.disconnect$.subscribe(() => {
|
this.ssh.disconnect$.subscribe(() => {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
this.destroy()
|
this.destroy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// auth
|
// Authentication
|
||||||
|
|
||||||
this.authUsername ??= this.profile.options.user
|
this.authUsername ??= this.profile.options.user
|
||||||
if (!this.authUsername) {
|
if (!this.authUsername) {
|
||||||
@ -276,24 +299,6 @@ export class SSHSession {
|
|||||||
this.passwordStorage.savePassword(this.profile, this.savedPassword)
|
this.passwordStorage.savePassword(this.profile, this.savedPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
//zone ?
|
|
||||||
|
|
||||||
// const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
|
|
||||||
|
|
||||||
|
|
||||||
// ssh.on('greeting', greeting => {
|
|
||||||
// if (!this.profile.options.skipBanner) {
|
|
||||||
// log('Greeting: ' + greeting)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// ssh.on('banner', banner => {
|
|
||||||
// if (!this.profile.options.skipBanner) {
|
|
||||||
// log(banner)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// if (this.profile.options.socksProxyHost) {
|
// if (this.profile.options.socksProxyHost) {
|
||||||
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
|
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
|
||||||
@ -303,21 +308,6 @@ export class SSHSession {
|
|||||||
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.httpProxyHost}:${this.profile.options.httpProxyPort}`)
|
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.httpProxyHost}:${this.profile.options.httpProxyPort}`)
|
||||||
// this.proxyCommandStream = new HTTPProxyStream(this.profile)
|
// this.proxyCommandStream = new HTTPProxyStream(this.profile)
|
||||||
// }
|
// }
|
||||||
// if (this.profile.options.proxyCommand) {
|
|
||||||
// this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
|
|
||||||
// this.proxyCommandStream = new ProxyCommandStream(this.profile.options.proxyCommand)
|
|
||||||
// }
|
|
||||||
// if (this.proxyCommandStream) {
|
|
||||||
// this.proxyCommandStream.destroyed$.subscribe(err => {
|
|
||||||
// if (err) {
|
|
||||||
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
|
||||||
// this.destroy()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// this.proxyCommandStream.message$.subscribe(message => {
|
|
||||||
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ' ' + message.trim())
|
|
||||||
// })
|
|
||||||
|
|
||||||
// await this.proxyCommandStream.start()
|
// await this.proxyCommandStream.start()
|
||||||
// }
|
// }
|
||||||
@ -327,59 +317,45 @@ export class SSHSession {
|
|||||||
// host: this.profile.options.host.trim(),
|
// host: this.profile.options.host.trim(),
|
||||||
// port: this.profile.options.port ?? 22,
|
// port: this.profile.options.port ?? 22,
|
||||||
// sock: this.proxyCommandStream?.socket ?? this.jumpStream,
|
// sock: this.proxyCommandStream?.socket ?? this.jumpStream,
|
||||||
// username: this.authUsername ?? undefined,
|
|
||||||
// tryKeyboard: true,
|
|
||||||
// agent: this.agentPath,
|
// agent: this.agentPath,
|
||||||
// agentForward: this.profile.options.agentForward && !!this.agentPath,
|
// agentForward: this.profile.options.agentForward && !!this.agentPath,
|
||||||
// keepaliveInterval: this.profile.options.keepaliveInterval ?? 15000,
|
// keepaliveInterval: this.profile.options.keepaliveInterval ?? 15000,
|
||||||
// keepaliveCountMax: this.profile.options.keepaliveCountMax,
|
// keepaliveCountMax: this.profile.options.keepaliveCountMax,
|
||||||
// readyTimeout: this.profile.options.readyTimeout,
|
// readyTimeout: this.profile.options.readyTimeout,
|
||||||
// algorithms,
|
|
||||||
// authHandler: (methodsLeft, partialSuccess, callback) => {
|
|
||||||
// this.zone.run(async () => {
|
|
||||||
// callback(await this.handleAuth(methodsLeft))
|
|
||||||
// })
|
|
||||||
// },
|
|
||||||
// })
|
// })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.notifications.error(e.message)
|
this.notifications.error(e.message)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
// for (const fw of this.profile.options.forwardedPorts ?? []) {
|
for (const fw of this.profile.options.forwardedPorts ?? []) {
|
||||||
// this.addPortForward(Object.assign(new ForwardedPort(), fw))
|
this.addPortForward(Object.assign(new ForwardedPort(), fw))
|
||||||
// }
|
}
|
||||||
|
|
||||||
this.open = true
|
this.open = true
|
||||||
|
|
||||||
// this.ssh.on('tcp connection', (details, accept, reject) => {
|
this.ssh.tcpChannelOpen$.subscribe(async event => {
|
||||||
// this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`)
|
this.logger.info(`Incoming forwarded connection: ${event.clientAddress}:${event.clientPort} -> ${event.targetAddress}:${event.targetPort}`)
|
||||||
// const forward = this.forwardedPorts.find(x => x.port === details.destPort)
|
const forward = this.forwardedPorts.find(x => x.port === event.targetPort && x.host === event.targetAddress)
|
||||||
// if (!forward) {
|
if (!forward) {
|
||||||
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`)
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${event.targetAddress}:${event.targetPort}`)
|
||||||
// reject()
|
return
|
||||||
// return
|
}
|
||||||
// }
|
const socket = new Socket()
|
||||||
// const socket = new Socket()
|
socket.connect(forward.targetPort, forward.targetAddress)
|
||||||
// socket.connect(forward.targetPort, forward.targetAddress)
|
socket.on('error', e => {
|
||||||
// socket.on('error', e => {
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
// // eslint-disable-next-line @typescript-eslint/no-base-to-string
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
|
||||||
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
|
event.channel.close()
|
||||||
// reject()
|
})
|
||||||
// })
|
event.channel.data$.subscribe(data => socket.write(data))
|
||||||
// socket.on('connect', () => {
|
socket.on('data', data => event.channel.write(Uint8Array.from(data)))
|
||||||
// this.logger.info('Connection forwarded')
|
event.channel.closed$.subscribe(() => socket.destroy())
|
||||||
// const stream = accept()
|
socket.on('close', () => event.channel.close())
|
||||||
// stream.pipe(socket)
|
socket.on('connect', () => {
|
||||||
// socket.pipe(stream)
|
this.logger.info('Connection forwarded')
|
||||||
// stream.on('close', () => {
|
})
|
||||||
// socket.destroy()
|
})
|
||||||
// })
|
|
||||||
// socket.on('close', () => {
|
|
||||||
// stream.close()
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
this.ssh.x11ChannelOpen$.subscribe(async event => {
|
this.ssh.x11ChannelOpen$.subscribe(async event => {
|
||||||
this.logger.info(`Incoming X11 connection from ${event.clientAddress}:${event.clientPort}`)
|
this.logger.info(`Incoming X11 connection from ${event.clientAddress}:${event.clientPort}`)
|
||||||
@ -541,7 +517,7 @@ export class SSHSession {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompts = await state.prompts()
|
const prompts = state.prompts()
|
||||||
|
|
||||||
let responses: string[] = []
|
let responses: string[] = []
|
||||||
// OpenSSH can send a k-i request without prompts
|
// OpenSSH can send a k-i request without prompts
|
||||||
@ -550,7 +526,7 @@ export class SSHSession {
|
|||||||
const prompt = new KeyboardInteractivePrompt(
|
const prompt = new KeyboardInteractivePrompt(
|
||||||
state.name,
|
state.name,
|
||||||
state.instructions,
|
state.instructions,
|
||||||
await state.prompts(),
|
state.prompts(),
|
||||||
)
|
)
|
||||||
this.emitKeyboardInteractivePrompt(prompt)
|
this.emitKeyboardInteractivePrompt(prompt)
|
||||||
|
|
||||||
@ -573,67 +549,66 @@ export class SSHSession {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPortForward (_fw: ForwardedPort): Promise<void> {
|
async addPortForward (fw: ForwardedPort): Promise<void> {
|
||||||
// if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
|
if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
|
||||||
// await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
|
await fw.startLocalListener(async (accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
|
||||||
// this.logger.info(`New connection on ${fw}`)
|
this.logger.info(`New connection on ${fw}`)
|
||||||
// this.ssh.forwardOut(
|
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
|
||||||
// sourceAddress ?? '127.0.0.1',
|
this.logger.error(`Connection while unauthenticated on ${fw}`)
|
||||||
// sourcePort ?? 0,
|
reject()
|
||||||
// targetAddress,
|
return
|
||||||
// targetPort,
|
}
|
||||||
// (err, stream) => {
|
const channel = await this.ssh.openTCPForwardChannel({
|
||||||
// if (err) {
|
addressToConnectTo: targetAddress,
|
||||||
// // eslint-disable-next-line @typescript-eslint/no-base-to-string
|
portToConnectTo: targetPort,
|
||||||
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
|
originatorAddress: sourceAddress ?? '127.0.0.1',
|
||||||
// reject()
|
originatorPort: sourcePort ?? 0,
|
||||||
// return
|
}).catch(err => {
|
||||||
// }
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
|
||||||
// const socket = accept()
|
reject()
|
||||||
// stream.pipe(socket)
|
throw err
|
||||||
// socket.pipe(stream)
|
})
|
||||||
// stream.on('close', () => {
|
const socket = accept()
|
||||||
// socket.destroy()
|
channel.data$.subscribe(data => socket.write(data))
|
||||||
// })
|
socket.on('data', data => channel.write(Uint8Array.from(data)))
|
||||||
// socket.on('close', () => {
|
channel.closed$.subscribe(() => socket.destroy())
|
||||||
// stream.close()
|
socket.on('close', () => channel.close())
|
||||||
// })
|
}).then(() => {
|
||||||
// },
|
this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`)
|
||||||
// )
|
this.forwardedPorts.push(fw)
|
||||||
// }).then(() => {
|
}).catch(e => {
|
||||||
// this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`)
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`)
|
||||||
// this.forwardedPorts.push(fw)
|
throw e
|
||||||
// }).catch(e => {
|
})
|
||||||
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`)
|
}
|
||||||
// throw e
|
if (fw.type === PortForwardType.Remote) {
|
||||||
// })
|
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
|
||||||
// }
|
throw new Error('Cannot add remote port forward before auth')
|
||||||
// if (fw.type === PortForwardType.Remote) {
|
}
|
||||||
// await new Promise<void>((resolve, reject) => {
|
try {
|
||||||
// this.ssh.forwardIn(fw.host, fw.port, err => {
|
await this.ssh.forwardTCPPort(fw.host, fw.port)
|
||||||
// if (err) {
|
} catch (err) {
|
||||||
// // eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
|
||||||
// reject(err)
|
return
|
||||||
// return
|
}
|
||||||
// }
|
this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
|
||||||
// resolve()
|
this.forwardedPorts.push(fw)
|
||||||
// })
|
}
|
||||||
// })
|
|
||||||
// this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
|
|
||||||
// this.forwardedPorts.push(fw)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async removePortForward (fw: ForwardedPort): Promise<void> {
|
async removePortForward (fw: ForwardedPort): Promise<void> {
|
||||||
// if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
|
if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
|
||||||
// fw.stopLocalListener()
|
fw.stopLocalListener()
|
||||||
// this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
||||||
// }
|
}
|
||||||
// if (fw.type === PortForwardType.Remote) {
|
if (fw.type === PortForwardType.Remote) {
|
||||||
// this.ssh.unforwardIn(fw.host, fw.port)
|
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
|
||||||
// this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
throw new Error('Cannot remove remote port forward before auth')
|
||||||
// }
|
}
|
||||||
|
this.ssh.stopForwardingTCPPort(fw.host, fw.port)
|
||||||
|
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
|
||||||
|
}
|
||||||
this.emitServiceMessage(`Stopped forwarding ${fw}`)
|
this.emitServiceMessage(`Stopped forwarding ${fw}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user