mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-13 07:59:59 +00: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
|
settingsComponent = TelnetProfileSettingsComponent
|
||||||
|
|
||||||
async getBuiltinProfiles (): Promise<TelnetProfile[]> {
|
async getBuiltinProfiles (): Promise<TelnetProfile[]> {
|
||||||
return [{
|
return [
|
||||||
id: `telnet:template`,
|
{
|
||||||
type: 'telnet',
|
id: `telnet:template`,
|
||||||
name: 'Telnet/socket connection',
|
type: 'telnet',
|
||||||
icon: 'fas fa-network-wired',
|
name: 'Telnet session',
|
||||||
options: {
|
icon: 'fas fa-network-wired',
|
||||||
host: '',
|
options: {
|
||||||
port: 23,
|
host: '',
|
||||||
inputMode: 'readline',
|
port: 23,
|
||||||
outputMode: null,
|
inputMode: 'readline',
|
||||||
inputNewlines: null,
|
outputMode: null,
|
||||||
outputNewlines: 'crlf',
|
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>> {
|
async getNewTabParameters (profile: Profile): Promise<NewTabParameters<TelnetTabComponent>> {
|
||||||
|
@ -16,12 +16,38 @@ export interface TelnetProfileOptions extends StreamProcessingOptions, LoginScri
|
|||||||
port?: number
|
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 {
|
export class TelnetSession extends BaseSession {
|
||||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||||
|
|
||||||
private serviceMessage = new Subject<string>()
|
private serviceMessage = new Subject<string>()
|
||||||
private socket: Socket
|
private socket: Socket
|
||||||
private streamProcessor: TerminalStreamProcessor
|
private streamProcessor: TerminalStreamProcessor
|
||||||
|
private telnetProtocol = false
|
||||||
|
private echoEnabled = false
|
||||||
|
private lastWidth = 0
|
||||||
|
private lastHeight = 0
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
injector: Injector,
|
injector: Injector,
|
||||||
@ -30,7 +56,7 @@ export class TelnetSession extends BaseSession {
|
|||||||
super(injector.get(LogService).create(`telnet-${profile.options.host}-${profile.options.port}`))
|
super(injector.get(LogService).create(`telnet-${profile.options.host}-${profile.options.port}`))
|
||||||
this.streamProcessor = new TerminalStreamProcessor(profile.options)
|
this.streamProcessor = new TerminalStreamProcessor(profile.options)
|
||||||
this.streamProcessor.outputToSession$.subscribe(data => {
|
this.streamProcessor.outputToSession$.subscribe(data => {
|
||||||
this.socket.write(data)
|
this.socket.write(this.unescapeFF(data))
|
||||||
})
|
})
|
||||||
this.streamProcessor.outputToTerminal$.subscribe(data => {
|
this.streamProcessor.outputToTerminal$.subscribe(data => {
|
||||||
this.emitOutput(data)
|
this.emitOutput(data)
|
||||||
@ -38,6 +64,24 @@ export class TelnetSession extends BaseSession {
|
|||||||
this.setLoginScriptsOptions(profile.options)
|
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> {
|
async start (): Promise<void> {
|
||||||
this.socket = new Socket()
|
this.socket = new Socket()
|
||||||
this.emitServiceMessage(`Connecting to ${this.profile.options.host}`)
|
this.emitServiceMessage(`Connecting to ${this.profile.options.host}`)
|
||||||
@ -52,7 +96,7 @@ export class TelnetSession extends BaseSession {
|
|||||||
this.emitServiceMessage('Connection closed')
|
this.emitServiceMessage('Connection closed')
|
||||||
this.destroy()
|
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.socket.connect(this.profile.options.port ?? 23, this.profile.options.host, () => {
|
||||||
this.emitServiceMessage('Connected')
|
this.emitServiceMessage('Connected')
|
||||||
this.open = true
|
this.open = true
|
||||||
@ -68,10 +112,115 @@ export class TelnetSession extends BaseSession {
|
|||||||
this.logger.info(stripAnsi(msg))
|
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
|
// 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 {
|
write (data: Buffer): void {
|
||||||
|
if (this.echoEnabled) {
|
||||||
|
this.emitOutput(data)
|
||||||
|
}
|
||||||
this.streamProcessor.feedFromTerminal(data)
|
this.streamProcessor.feedFromTerminal(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user