mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-17 17:09:54 +00:00
274 lines
9.2 KiB
TypeScript
274 lines
9.2 KiB
TypeScript
import { Socket } from 'net'
|
|
import colors from 'ansi-colors'
|
|
import stripAnsi from 'strip-ansi'
|
|
import { Injector } from '@angular/core'
|
|
import { Profile, LogService } from 'tabby-core'
|
|
import { BaseSession, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
|
|
import { Subject, Observable } from 'rxjs'
|
|
|
|
|
|
export interface TelnetProfile extends Profile {
|
|
options: TelnetProfileOptions
|
|
}
|
|
|
|
export interface TelnetProfileOptions extends StreamProcessingOptions, LoginScriptsOptions {
|
|
host: string
|
|
port?: number
|
|
}
|
|
|
|
enum TelnetCommands {
|
|
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,
|
|
REMOTE_FLOW_CONTROL = 0x21,
|
|
X_DISPLAY_LOCATION = 0x23,
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
private requestedOptions = new Set<number>()
|
|
|
|
constructor (
|
|
injector: Injector,
|
|
public profile: TelnetProfile,
|
|
) {
|
|
super(injector.get(LogService).create(`telnet-${profile.options.host}-${profile.options.port}`))
|
|
this.streamProcessor = new TerminalStreamProcessor(profile.options)
|
|
this.middleware.push(this.streamProcessor)
|
|
this.setLoginScriptsOptions(profile.options)
|
|
}
|
|
|
|
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}`)
|
|
reject(err)
|
|
this.destroy()
|
|
})
|
|
this.socket.on('close', () => {
|
|
this.emitServiceMessage('Connection closed')
|
|
this.destroy()
|
|
})
|
|
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
|
|
setTimeout(() => this.streamProcessor.start())
|
|
this.loginScriptProcessor?.executeUnconditionalScripts()
|
|
resolve()
|
|
})
|
|
})
|
|
}
|
|
|
|
requestOption (cmd: TelnetCommands, option: TelnetOptions): void {
|
|
this.requestedOptions.add(option)
|
|
this.emitTelnet(cmd, option)
|
|
}
|
|
|
|
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())
|
|
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 {
|
|
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)
|
|
|
|
if (command === TelnetCommands.WILL || command === TelnetCommands.WONT) {
|
|
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)
|
|
} 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)
|
|
this.emitSize()
|
|
} else if (option === TelnetOptions.ECHO) {
|
|
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')
|
|
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)
|
|
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
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,
|
|
]))
|
|
} else {
|
|
this.emitTelnet(TelnetCommands.WONT, TelnetOptions.NEGO_WINDOW_SIZE)
|
|
}
|
|
}
|
|
|
|
write (data: Buffer): void {
|
|
this.socket.write(data)
|
|
}
|
|
|
|
kill (_signal?: string): void {
|
|
this.socket.destroy()
|
|
}
|
|
|
|
async destroy (): Promise<void> {
|
|
this.streamProcessor.close()
|
|
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
|
|
}
|
|
}
|