tabby/tabby-telnet/src/session.ts

297 lines
10 KiB
TypeScript
Raw Permalink Normal View History

2023-08-26 05:40:36 +08:00
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
2021-07-04 22:48:48 +08:00
import { Socket } from 'net'
import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi'
import { Injector } from '@angular/core'
import { LogService } from 'tabby-core'
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
2021-07-04 22:48:48 +08:00
import { Subject, Observable } from 'rxjs'
export interface TelnetProfile extends ConnectableTerminalProfile {
2021-07-04 22:48:48 +08:00
options: TelnetProfileOptions
}
export interface TelnetProfileOptions extends StreamProcessingOptions, LoginScriptsOptions {
2021-07-04 22:48:48 +08:00
host: string
port?: number
input: InputProcessingOptions,
2021-07-04 22:48:48 +08:00
}
enum TelnetCommands {
2021-09-17 06:23:04 +08:00
SUBOPTION_SEND = 1,
SUBOPTION_END = 240,
GA = 249,
SUBOPTION = 250,
WILL = 251,
WONT = 252,
DO = 253,
DONT = 254,
IAC = 255,
}
enum TelnetOptions {
ECHO = 0x1,
AUTH_OPTIONS = 0x25,
SUPPRESS_GO_AHEAD = 0x03,
TERMINAL_TYPE = 0x18,
NEGO_WINDOW_SIZE = 0x1f,
NEGO_TERMINAL_SPEED = 0x20,
STATUS = 0x05,
2021-09-17 06:23:04 +08:00
REMOTE_FLOW_CONTROL = 0x21,
X_DISPLAY_LOCATION = 0x23,
2021-09-17 06:23:04 +08:00
NEW_ENVIRON = 0x27,
}
class UnescapeFFMiddleware extends SessionMiddleware {
feedFromSession (data: Buffer): void {
while (data.includes(0xff)) {
const pos = data.indexOf(0xff)
this.outputToTerminal.next(data.slice(0, pos))
this.outputToTerminal.next(Buffer.from([0xff, 0xff]))
data = data.slice(pos + 1)
}
this.outputToTerminal.next(data)
}
}
2021-07-04 22:48:48 +08:00
export class TelnetSession extends BaseSession {
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
private serviceMessage = new Subject<string>()
private socket: Socket
private streamProcessor: TerminalStreamProcessor
private telnetProtocol = false
private lastWidth = 0
private lastHeight = 0
2021-09-17 06:23:04 +08:00
private requestedOptions = new Set<number>()
2023-07-10 13:48:48 +08:00
private telnetRemoteEcho = false
2021-07-04 22:48:48 +08:00
constructor (
injector: Injector,
public profile: TelnetProfile,
) {
super(injector.get(LogService).create(`telnet-${profile.options.host}-${profile.options.port}`))
2021-07-04 22:48:48 +08:00
this.streamProcessor = new TerminalStreamProcessor(profile.options)
this.middleware.push(this.streamProcessor)
this.middleware.push(new InputProcessor(profile.options.input))
this.setLoginScriptsOptions(profile.options)
2021-07-04 22:48:48 +08:00
}
async start (): Promise<void> {
this.socket = new Socket()
this.emitServiceMessage(`Connecting to ${this.profile.options.host}`)
return new Promise((resolve, reject) => {
this.socket.on('error', err => {
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Socket error: ${err as any}`)
2021-07-06 04:27:58 +08:00
reject(err)
2021-07-04 22:48:48 +08:00
this.destroy()
})
this.socket.on('close', () => {
this.emitServiceMessage('Connection closed')
this.destroy()
})
this.socket.on('data', data => this.onData(data))
2021-07-04 22:48:48 +08:00
this.socket.connect(this.profile.options.port ?? 23, this.profile.options.host, () => {
this.emitServiceMessage('Connected')
this.open = true
setTimeout(() => this.streamProcessor.start())
this.loginScriptProcessor?.executeUnconditionalScripts()
2021-07-04 22:48:48 +08:00
resolve()
})
})
}
2021-09-17 06:23:04 +08:00
requestOption (cmd: TelnetCommands, option: TelnetOptions): void {
this.requestedOptions.add(option)
this.emitTelnet(cmd, option)
}
2021-07-04 22:48:48 +08:00
emitServiceMessage (msg: string): void {
this.serviceMessage.next(msg)
this.logger.info(stripAnsi(msg))
}
onData (data: Buffer): void {
if (!this.telnetProtocol && data[0] === TelnetCommands.IAC) {
this.telnetProtocol = true
this.middleware.push(new UnescapeFFMiddleware())
2021-09-17 06:23:04 +08:00
this.requestOption(TelnetCommands.DO, TelnetOptions.SUPPRESS_GO_AHEAD)
this.emitTelnet(TelnetCommands.WILL, TelnetOptions.TERMINAL_TYPE)
this.emitTelnet(TelnetCommands.WILL, TelnetOptions.NEGO_WINDOW_SIZE)
}
if (this.telnetProtocol) {
data = this.processTelnetProtocol(data)
}
this.emitOutput(data)
}
emitTelnet (command: TelnetCommands, option: TelnetOptions): void {
2021-09-17 06:23:04 +08:00
this.logger.debug('>', TelnetCommands[command], TelnetOptions[option] || option)
this.socket.write(Buffer.from([TelnetCommands.IAC, command, option]))
}
emitTelnetSuboption (option: TelnetOptions, value: Buffer): void {
this.logger.debug('>', 'SUBOPTION', TelnetOptions[option], value)
this.socket.write(Buffer.from([
TelnetCommands.IAC,
TelnetCommands.SUBOPTION,
option,
...value,
TelnetCommands.IAC,
TelnetCommands.SUBOPTION_END,
]))
}
processTelnetProtocol (data: Buffer): Buffer {
while (data.length) {
if (data[0] === TelnetCommands.IAC) {
const command = data[1]
const commandName = TelnetCommands[command]
const option = data[2]
const optionName = TelnetOptions[option]
if (command === TelnetCommands.IAC) {
data = data.slice(1)
break
}
data = data.slice(3)
this.logger.debug('<', commandName || command, optionName || option)
2021-09-17 06:23:04 +08:00
2023-07-10 13:48:48 +08:00
if (command === TelnetCommands.WILL || command === TelnetCommands.WONT || command === TelnetCommands.DONT) {
2021-09-17 06:23:04 +08:00
if (this.requestedOptions.has(option)) {
this.requestedOptions.delete(option)
continue
}
}
if (command === TelnetCommands.WILL) {
if ([
TelnetOptions.SUPPRESS_GO_AHEAD,
TelnetOptions.ECHO,
].includes(option)) {
this.emitTelnet(TelnetCommands.DO, option)
2023-07-10 13:48:48 +08:00
if (option === TelnetOptions.ECHO && this.streamProcessor.forceEcho) {
2023-07-10 14:10:54 +08:00
this.telnetRemoteEcho = true
this.streamProcessor.forceEcho = false
this.requestOption(TelnetCommands.WONT, option)
2023-07-10 13:48:48 +08:00
}
} else {
this.logger.debug('(!) Unhandled option')
this.emitTelnet(TelnetCommands.DONT, option)
}
}
if (command === TelnetCommands.DO) {
if (option === TelnetOptions.NEGO_WINDOW_SIZE) {
this.emitTelnet(TelnetCommands.WILL, option)
2021-09-17 06:23:04 +08:00
this.emitSize()
} else if (option === TelnetOptions.ECHO) {
2023-07-10 13:48:48 +08:00
if (this.telnetRemoteEcho) {
2023-07-10 14:10:54 +08:00
this.streamProcessor.forceEcho = false
2023-07-10 13:48:48 +08:00
this.emitTelnet(TelnetCommands.WONT, option)
} else {
this.streamProcessor.forceEcho = true
this.emitTelnet(TelnetCommands.WILL, option)
}
} else if (option === TelnetOptions.TERMINAL_TYPE) {
this.emitTelnet(TelnetCommands.WILL, option)
} else {
this.logger.debug('(!) Unhandled option')
this.emitTelnet(TelnetCommands.WONT, option)
}
}
if (command === TelnetCommands.DONT) {
if (option === TelnetOptions.ECHO) {
this.streamProcessor.forceEcho = false
this.emitTelnet(TelnetCommands.WONT, option)
} else {
this.logger.debug('(!) Unhandled option')
2023-07-10 13:48:48 +08:00
this.emitTelnet(TelnetCommands.WONT, option)
}
}
if (command === TelnetCommands.WONT) {
if (option === TelnetOptions.ECHO) {
2023-07-10 14:10:54 +08:00
this.telnetRemoteEcho = false
2023-07-10 13:48:48 +08:00
this.emitTelnet(TelnetCommands.DONT, option)
} else {
this.logger.debug('(!) Unhandled option')
this.emitTelnet(TelnetCommands.DONT, option)
}
}
if (command === TelnetCommands.SUBOPTION) {
const endIndex = data.indexOf(TelnetCommands.IAC)
const optionValue = data.slice(0, endIndex)
this.logger.debug('<', commandName || command, optionName || option, optionValue)
2021-09-17 06:23:04 +08:00
if (option === TelnetOptions.TERMINAL_TYPE && optionValue[0] === TelnetCommands.SUBOPTION_SEND) {
this.emitTelnetSuboption(option, Buffer.from([0, ...Buffer.from('XTERM-256COLOR')]))
}
data = data.slice(endIndex + 2)
}
} else {
return data
}
}
return data
}
2021-07-04 22:48:48 +08:00
// eslint-disable-next-line @typescript-eslint/no-empty-function
resize (w: number, h: number): void {
if (w && h) {
this.lastWidth = w
this.lastHeight = h
}
if (this.lastWidth && this.lastHeight && this.telnetProtocol) {
2021-08-16 01:58:28 +08:00
this.emitSize()
}
}
private emitSize () {
if (this.lastWidth && this.lastHeight) {
this.emitTelnetSuboption(TelnetOptions.NEGO_WINDOW_SIZE, Buffer.from([
this.lastWidth >> 8, this.lastWidth & 0xff,
this.lastHeight >> 8, this.lastHeight & 0xff,
]))
2021-08-16 01:58:28 +08:00
} else {
this.emitTelnet(TelnetCommands.WONT, TelnetOptions.NEGO_WINDOW_SIZE)
}
}
2021-07-04 22:48:48 +08:00
write (data: Buffer): void {
this.socket.write(data)
2021-07-04 22:48:48 +08:00
}
kill (_signal?: string): void {
this.socket.destroy()
}
async destroy (): Promise<void> {
2021-07-09 16:31:12 +08:00
this.streamProcessor.close()
2021-07-04 22:48:48 +08:00
this.serviceMessage.complete()
this.kill()
await super.destroy()
}
async getChildProcesses (): Promise<any[]> {
return []
}
async gracefullyKillProcess (): Promise<void> {
this.kill()
}
supportsWorkingDirectory (): boolean {
return false
}
async getWorkingDirectory (): Promise<string|null> {
return null
}
}