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:
parent
f39b4c6dbe
commit
e1f2e176ce
@ -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>> {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user