1
1
mirror of https://github.com/Eugeny/tabby.git synced 2024-12-22 18:11:43 +03:00

actually implement telnet protocol - fixes #4164

This commit is contained in:
Eugene Pankov 2021-07-10 16:09:24 +02:00
parent f39b4c6dbe
commit e1f2e176ce
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
2 changed files with 181 additions and 18 deletions

View File

@ -12,22 +12,36 @@ export class TelnetProfilesService extends ProfileProvider {
settingsComponent = TelnetProfileSettingsComponent
async getBuiltinProfiles (): Promise<TelnetProfile[]> {
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<NewTabParameters<TelnetTabComponent>> {

View File

@ -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<string> { return this.serviceMessage }
private serviceMessage = new Subject<string>()
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<void> {
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)
}