// Sshwifty - A Web SSH client // // Copyright (C) 2019 Rui NI // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import * as iconv from "iconv-lite"; import * as subscribe from "../stream/subscribe.js"; import * as reader from "../stream/reader.js"; import * as color from "../commands/color.js"; import Exception from "../commands/exception.js"; // const maxReadBufSize = 1024; const cmdSE = 240; // const cmdNOP = 241; // const cmdDataMark = 242; // const cmdBreak = 243; // const cmdInterrputProcess = 244; // const cmdAbortOutput = 245; // const cmdAreYouThere = 246; // const cmdEraseCharacter = 247; // const cmdEraseLine = 248; const cmdGoAhead = 249; const cmdSB = 250; const cmdWill = 251; const cmdWont = 252; const cmdDo = 253; const cmdDont = 254; const cmdIAC = 255; const optEcho = 1; const optSuppressGoAhead = 3; const optTerminalType = 24; const optNAWS = 31; const optTerminalTypeIs = 0; const optTerminalTypeSend = 1; const unknownTermTypeSendData = new Uint8Array([ optTerminalTypeIs, 88, 84, 69, 82, 77 ]); // Most of code of this class is directly from // https://github.com/ziutek/telnet/blob/master/conn.go#L122 // Thank you! class Parser { constructor(sender, flusher, callbacks) { this.sender = sender; this.flusher = flusher; this.callbacks = callbacks; this.reader = new reader.Multiple(() => {}); this.options = { echoEnabled: false, suppressGoAhead: false, nawsAccpeted: false }; this.current = 0; } sendNego(cmd, option) { return this.sender(new Uint8Array([cmdIAC, cmd, option])); } sendDeny(cmd, o) { switch (cmd) { case cmdDo: return this.sendNego(cmdWont, o); case (cmdWill, cmdWont): return this.sendNego(cmdDont, o); } } sendWillSubNego(willCmd, data, option) { let b = new Uint8Array(6 + data.length + 2); b.set([cmdIAC, willCmd, option, cmdIAC, cmdSB, option], 0); b.set(data, 6); b.set([cmdIAC, cmdSE], data.length + 6); return this.sender(b); } sendSubNego(data, option) { let b = new Uint8Array(3 + data.length + 2); b.set([cmdIAC, cmdSB, option], 0); b.set(data, 3); b.set([cmdIAC, cmdSE], data.length + 3); return this.sender(b); } async handleTermTypeSubNego(rd) { let action = await reader.readOne(rd); if (action[0] !== optTerminalTypeSend) { return null; } let self = this; return () => { self.sendSubNego(unknownTermTypeSendData, optTerminalType); }; } async handleSubNego(rd) { let endExec = null; for (;;) { let d = await reader.readOne(rd); switch (d[0]) { case optTerminalType: endExec = await this.handleTermTypeSubNego(rd); continue; case cmdIAC: break; default: continue; } let e = await reader.readOne(rd); if (e[0] !== cmdSE) { continue; } if (endExec !== null) { endExec(); } return; } } handleOption(cmd, option, oldVal, newVal) { switch (cmd) { case cmdWill: if (!oldVal) { this.sendNego(cmdDo, option); } newVal(true, cmdWill); return; case cmdWont: if (oldVal) { this.sendNego(cmdDont, option); } newVal(false, cmdWont); return; case cmdDo: if (!oldVal) { this.sendNego(cmdWill, option); } newVal(true, cmdDo); return; case cmdDont: if (oldVal) { this.sendNego(cmdWont, option); } newVal(false, cmdDont); return; } } async handleCmd(rd) { let d = await reader.readOne(rd); switch (d[0]) { case cmdWill: case cmdWont: case cmdDo: case cmdDont: break; case cmdIAC: this.flusher(d); return; case cmdGoAhead: return; case cmdSB: await this.handleSubNego(rd); return; default: throw new Exception("Unknown command"); } let o = await reader.readOne(rd); switch (o[0]) { case optEcho: return this.handleOption( d[0], o[0], this.options.echoEnabled, (d, action) => { this.options.echoEnabled = d; switch (action) { case cmdWill: case cmdDont: this.callbacks.setEcho(false); break; case cmdWont: case cmdDo: this.callbacks.setEcho(true); break; } } ); case optSuppressGoAhead: return this.handleOption( d[0], o[0], this.options.suppressGoAhead, (d, _action) => { this.options.suppressGoAhead = d; } ); case optNAWS: // Window resize allowed? if (d[0] !== cmdDo) { this.sendDeny(d[0], o[0]); return; } let dim = this.callbacks.getWindowDim(), dimData = new DataView(new ArrayBuffer(4)); dimData.setUint16(0, dim.cols); dimData.setUint16(2, dim.rows); let dimBytes = new Uint8Array(dimData.buffer); if (this.options.nawsAccpeted) { this.sendSubNego(dimBytes, optNAWS); return; } this.options.nawsAccpeted = true; this.sendWillSubNego(cmdWill, dimBytes, optNAWS); return; case optTerminalType: if (d[0] !== cmdDo) { this.sendDeny(d[0], o[0]); return; } this.sendNego(cmdWill, o[0]); return; } this.sendDeny(d[0], o[0]); } requestWindowResize() { this.options.nawsAccpeted = true; this.sendNego(cmdWill, optNAWS); } async run() { try { for (;;) { let d = await reader.readUntil(this.reader, cmdIAC); if (!d.found) { this.flusher(d.data); continue; } if (d.data.length > 1) { this.flusher(d.data.slice(0, d.data.length - 1)); } await this.handleCmd(this.reader); } } catch (e) { // Do nothing } } feed(rd, cb) { this.reader.feed(rd, cb); } close() { this.reader.close(); } } class Control { constructor(data, color) { this.colorM = color; this.colors = this.colorM.get(); this.charset = data.charset; if (this.charset === "utf-8") { this.charsetDecoder = d => { return d; }; } else { let dec = new TextDecoder(this.charset), enc = new TextEncoder(); this.charsetDecoder = d => { return enc.encode( dec.decode(d, { stream: true }) ); }; } this.sender = data.send; this.closer = data.close; this.closed = false; this.localEchoEnabled = true; this.subs = new subscribe.Subscribe(); this.enable = false; this.windowDim = { cols: 65535, rows: 65535 }; let self = this; this.parser = new Parser( this.sender, d => { self.subs.resolve(this.charsetDecoder(d)); }, { setEcho(newVal) { self.localEchoEnabled = newVal; }, getWindowDim() { return self.windowDim; } } ); let runWait = this.parser.run(); data.events.place("inband", rd => { return new Promise((resolve, _reject) => { self.parser.feed(rd, () => { resolve(true); }); }); }); data.events.place("completed", async () => { self.parser.close(); self.closed = true; self.colorM.forget(self.colors.color); await runWait; self.subs.reject("Remote connection has been terminated"); }); } echo() { return this.localEchoEnabled; } resize(dim) { if (this.closed) { return; } this.windowDim.cols = dim.cols; this.windowDim.rows = dim.rows; this.parser.requestWindowResize(); } ui() { return "Console"; } enabled() { this.enable = true; } disabled() { this.enable = false; } retap(_isOn) {} receive() { return this.subs.subscribe(); } searchNextIAC(start, data) { for (let i = start; i < data.length; i++) { if (data[i] !== cmdIAC) { continue; } return i; } return -1; } send(data) { if (this.closed) { return; } let currentLen = 0; let enc = null; if (this.charset !== "utf-8") { enc = new iconv.encode(data, this.charset); } else { enc = new TextEncoder().encode(data); } while (currentLen < enc.length) { const iacPos = this.searchNextIAC(currentLen, enc); if (iacPos < 0) { this.sender(enc.slice(currentLen, enc.length)); return; } this.sender(enc.slice(currentLen, iacPos + 1)); this.sender(enc.slice(iacPos, iacPos + 1)); currentLen = iacPos + 1; } } color() { return this.colors.dark; } activeColor() { return this.colors.color; } close() { if (this.closer === null) { return; } let cc = this.closer; this.closer = null; return cc(); } } export class Telnet { /** * constructor * * @param {color.Color} c */ constructor(c) { this.color = c; } type() { return "Telnet"; } build(data) { return new Control(data, this.color); } }