From e1f2e176ce57d1fca95b5498c9e3ea646a64fb72 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sat, 10 Jul 2021 16:09:24 +0200 Subject: [PATCH] actually implement telnet protocol - fixes #4164 --- tabby-telnet/src/profiles.ts | 44 ++++++---- tabby-telnet/src/session.ts | 155 ++++++++++++++++++++++++++++++++++- 2 files changed, 181 insertions(+), 18 deletions(-) diff --git a/tabby-telnet/src/profiles.ts b/tabby-telnet/src/profiles.ts index 58bdf115..44b906aa 100644 --- a/tabby-telnet/src/profiles.ts +++ b/tabby-telnet/src/profiles.ts @@ -12,22 +12,36 @@ export class TelnetProfilesService extends ProfileProvider { settingsComponent = TelnetProfileSettingsComponent async getBuiltinProfiles (): Promise { - return [{ - id: `telnet:template`, - type: 'telnet', - name: 'Telnet/socket connection', - icon: 'fas fa-network-wired', - options: { - host: '', - port: 23, - inputMode: 'readline', - outputMode: null, - inputNewlines: null, - outputNewlines: 'crlf', + return [ + { + id: `telnet:template`, + type: 'telnet', + name: 'Telnet session', + icon: 'fas fa-network-wired', + options: { + host: '', + port: 23, + inputMode: 'readline', + outputMode: null, + inputNewlines: null, + outputNewlines: 'crlf', + }, + isBuiltin: true, + isTemplate: true, }, - isBuiltin: true, - isTemplate: true, - }] + { + id: `socket:template`, + type: 'telnet', + name: 'Raw socket connection', + icon: 'fas fa-network-wired', + options: { + host: '', + port: 1234, + }, + isBuiltin: true, + isTemplate: true, + }, + ] } async getNewTabParameters (profile: Profile): Promise> { diff --git a/tabby-telnet/src/session.ts b/tabby-telnet/src/session.ts index 99b68c25..f83fbf9f 100644 --- a/tabby-telnet/src/session.ts +++ b/tabby-telnet/src/session.ts @@ -16,12 +16,38 @@ export interface TelnetProfileOptions extends StreamProcessingOptions, LoginScri port?: number } +enum TelnetCommands { + 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, + X_DISPLAY_LOCATION = 0x23, +} + export class TelnetSession extends BaseSession { get serviceMessage$ (): Observable { return this.serviceMessage } private serviceMessage = new Subject() private socket: Socket private streamProcessor: TerminalStreamProcessor + private telnetProtocol = false + private echoEnabled = false + private lastWidth = 0 + private lastHeight = 0 constructor ( injector: Injector, @@ -30,7 +56,7 @@ export class TelnetSession extends BaseSession { super(injector.get(LogService).create(`telnet-${profile.options.host}-${profile.options.port}`)) this.streamProcessor = new TerminalStreamProcessor(profile.options) this.streamProcessor.outputToSession$.subscribe(data => { - this.socket.write(data) + this.socket.write(this.unescapeFF(data)) }) this.streamProcessor.outputToTerminal$.subscribe(data => { this.emitOutput(data) @@ -38,6 +64,24 @@ export class TelnetSession extends BaseSession { this.setLoginScriptsOptions(profile.options) } + unescapeFF (data: Buffer): Buffer { + if (!this.telnetProtocol) { + return data + } + const result: Buffer[] = [] + while (data.includes(0xff)) { + const pos = data.indexOf(0xff) + + result.push(data.slice(0, pos)) + result.push(Buffer.from([0xff, 0xff])) + + data = data.slice(pos + 1) + } + + result.push(data) + return Buffer.concat(result) + } + async start (): Promise { this.socket = new Socket() this.emitServiceMessage(`Connecting to ${this.profile.options.host}`) @@ -52,7 +96,7 @@ export class TelnetSession extends BaseSession { this.emitServiceMessage('Connection closed') this.destroy() }) - this.socket.on('data', data => this.streamProcessor.feedFromSession(data)) + this.socket.on('data', data => this.onData(data)) this.socket.connect(this.profile.options.port ?? 23, this.profile.options.host, () => { this.emitServiceMessage('Connected') this.open = true @@ -68,10 +112,115 @@ export class TelnetSession extends BaseSession { this.logger.info(stripAnsi(msg)) } + onData (data: Buffer): void { + if (!this.telnetProtocol && data[0] === TelnetCommands.IAC) { + this.telnetProtocol = true + this.emitTelnet(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.streamProcessor.feedFromSession(data) + } + + emitTelnet (command: TelnetCommands, option: TelnetOptions): void { + this.logger.debug('>', TelnetCommands[command], TelnetOptions[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) + if (command === TelnetCommands.WILL) { + if ([ + TelnetOptions.SUPPRESS_GO_AHEAD, + TelnetOptions.ECHO, + ].includes(option)) { + this.emitTelnet(TelnetCommands.DO, option) + } else { + this.logger.debug('(!) Unhandled option') + this.emitTelnet(TelnetCommands.DONT, option) + } + } + if (command === TelnetCommands.DO) { + if (option === TelnetOptions.NEGO_WINDOW_SIZE) { + this.resize(0, 0) + } else if (option === TelnetOptions.ECHO) { + this.echoEnabled = true + this.emitTelnet(TelnetCommands.WILL, option) + } else if (option === TelnetOptions.TERMINAL_TYPE) { + this.emitTelnetSuboption(option, Buffer.from([0, ...Buffer.from('XTERM-256COLOR')])) + } else { + this.logger.debug('(!) Unhandled option') + this.emitTelnet(TelnetCommands.WONT, option) + } + } + if (command === TelnetCommands.DONT) { + if (option === TelnetOptions.ECHO) { + this.echoEnabled = false + this.emitTelnet(TelnetCommands.WONT, option) + } else { + this.logger.debug('(!) Unhandled option') + this.emitTelnet(TelnetCommands.WILL, 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) + data = data.slice(endIndex + 2) + } + } else { + return data + } + } + return data + } + // eslint-disable-next-line @typescript-eslint/no-empty-function - resize (_w: number, _h: number): void { } + resize (w: number, h: number): void { + if (w && h) { + this.lastWidth = w + this.lastHeight = h + } + if (this.lastWidth && this.lastHeight && this.telnetProtocol) { + this.emitTelnetSuboption(TelnetOptions.NEGO_WINDOW_SIZE, Buffer.from([ + this.lastWidth >> 8, this.lastWidth & 0xff, + this.lastHeight >> 8, this.lastHeight & 0xff, + ])) + } + } write (data: Buffer): void { + if (this.echoEnabled) { + this.emitOutput(data) + } this.streamProcessor.feedFromTerminal(data) }