// 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 address from "./address.js"; import * as command from "./commands.js"; import * as common from "./common.js"; import * as event from "./events.js"; import * as reader from "../stream/reader.js"; import * as stream from "../stream/stream.js"; import * as controls from "./controls.js"; import * as history from "./history.js"; import * as header from "../stream/header.js"; import Exception from "./exception.js"; const COMMAND_ID = 0x00; const SERVER_INITIAL_ERROR_BAD_ADDRESS = 0x01; const SERVER_REMOTE_BAND = 0x00; const SERVER_DIAL_FAILED = 0x01; const SERVER_DIAL_CONNECTED = 0x02; const DEFAULT_PORT = 23; class Telnet { /** * constructor * * @param {stream.Sender} sd Stream sender * @param {object} config configuration * @param {object} callbacks Event callbacks * */ constructor(sd, config, callbacks) { this.sender = sd; this.config = config; this.connected = false; this.events = new event.Events( [ "initialization.failed", "initialized", "connect.failed", "connect.succeed", "@inband", "close", "@completed" ], callbacks ); } /** * Send intial request * * @param {stream.InitialSender} initialSender Initial stream request sender * */ run(initialSender) { let addr = new address.Address( this.config.host.type, this.config.host.address, this.config.host.port ), addrBuf = addr.buffer(); let data = new Uint8Array(addrBuf.length); data.set(addrBuf, 0); initialSender.send(data); } /** * Receive the initial stream request * * @param {header.InitialStream} streamInitialHeader Server respond on the * initial stream request * */ initialize(streamInitialHeader) { if (!streamInitialHeader.success()) { this.events.fire("initialization.failed", streamInitialHeader); return; } this.events.fire("initialized", streamInitialHeader); } /** * Tick the command * * @param {header.Stream} streamHeader Stream data header * @param {reader.Limited} rd Data reader * * @returns {any} The result of the ticking * * @throws {Exception} When the stream header type is unknown * */ tick(streamHeader, rd) { switch (streamHeader.marker()) { case SERVER_DIAL_CONNECTED: if (!this.connected) { this.connected = true; return this.events.fire("connect.succeed", rd, this); } break; case SERVER_DIAL_FAILED: if (!this.connected) { return this.events.fire("connect.failed", rd); } break; case SERVER_REMOTE_BAND: if (this.connected) { return this.events.fire("inband", rd); } break; } throw new Exception("Unknown stream header marker"); } /** * Send close signal to remote * */ sendClose() { return this.sender.close(); } /** * Send data to remote * * @param {Uint8Array} data * */ sendData(data) { return this.sender.send(0x00, data); } /** * Close the command * */ close() { this.sendClose(); return this.events.fire("close"); } /** * Tear down the command completely * */ completed() { return this.events.fire("completed"); } } const initialFieldDef = { Host: { name: "Host", description: "Looking for server to connect? Checkout " + '' + "telnet.org for public servers.", type: "text", value: "", example: "telnet.vaguly.com:23", verify(d) { if (d.length <= 0) { throw new Error("Hostname must be specified"); } let addr = common.splitHostPort(d, DEFAULT_PORT); if (addr.addr.length <= 0) { throw new Error("Cannot be empty"); } if (addr.addr.length > address.MAX_ADDR_LEN) { throw new Error( "Can no longer than " + address.MAX_ADDR_LEN + " bytes" ); } if (addr.port <= 0) { throw new Error("Port must be specified"); } return "Look like " + addr.type + " address"; } } }; class Wizard { /** * constructor * * @param {command.Info} info * @param {object} config * @param {streams.Streams} streams * @param {subscribe.Subscribe} subs * @param {controls.Controls} controls * @param {history.History} history * */ constructor(info, config, streams, subs, controls, history) { this.info = info; this.hasStarted = false; this.streams = streams; this.config = config; this.step = subs; this.controls = controls; this.history = history; this.step.resolve(this.stepInitialPrompt()); } started() { return this.hasStarted; } close() { this.step.resolve( this.stepErrorDone( "Action cancelled", "Action has been cancelled without reach any success" ) ); } stepErrorDone(title, message) { return command.done(false, null, title, message); } stepSuccessfulDone(data) { return command.done( true, data, "Success!", "We have connected to the remote" ); } stepWaitForAcceptWait() { return command.wait( "Requesting", "Waiting for request to be accepted by the backend" ); } stepWaitForEstablishWait(host) { return command.wait( "Connecting to " + host, "Establishing connection with the remote host, may take a while" ); } /** * * @param {stream.Sender} sender * @param {object} configInput * */ buildCommand(sender, configInput) { let self = this; let parsedConfig = { host: address.parseHostPort(configInput.host, DEFAULT_PORT) }; return new Telnet(sender, parsedConfig, { "initialization.failed"(streamInitialHeader) { switch (streamInitialHeader.data()) { case SERVER_INITIAL_ERROR_BAD_ADDRESS: self.step.resolve( self.stepErrorDone("Request rejected", "Invalid address") ); return; } self.step.resolve( self.stepErrorDone( "Request rejected", "Unknown error code: " + streamInitialHeader.data() ) ); }, initialized(streamInitialHeader) { self.step.resolve(self.stepWaitForEstablishWait(configInput.host)); }, "connect.succeed"(rd, commandHandler) { self.step.resolve( self.stepSuccessfulDone( new command.Result( configInput.host, self.info, self.controls.get("Telnet", { send(data) { return commandHandler.sendData(data); }, close() { return commandHandler.sendClose(); }, events: commandHandler.events }) ) ) ); self.history.save( self.info.name() + ":" + configInput.host, configInput.host, new Date(), self.info, configInput ); }, async "connect.failed"(rd) { let readed = await reader.readCompletely(rd), message = new TextDecoder("utf-8").decode(readed.buffer); self.step.resolve(self.stepErrorDone("Connection failed", message)); }, "@inband"(rd) {}, close() {}, "@completed"() {} }); } stepInitialPrompt() { let self = this; if (this.config) { self.hasStarted = true; self.streams.request(COMMAND_ID, sd => { return self.buildCommand(sd, this.config); }); return self.stepWaitForAcceptWait(); } return command.prompt( "Telnet", "Teletype Network", "Connect", r => { self.hasStarted = true; self.streams.request(COMMAND_ID, sd => { return self.buildCommand(sd, r); }); self.step.resolve(self.stepWaitForAcceptWait()); }, () => {}, command.fields(initialFieldDef, [{ name: "Host" }]) ); } } export class Command { constructor() {} id() { return COMMAND_ID; } name() { return "Telnet"; } description() { return "Teletype Network"; } color() { return "#6ac"; } builder(info, config, streams, subs, controls, history) { return new Wizard(info, config, streams, subs, controls, history); } launch(info, launcher, streams, subs, controls, history) { try { initialFieldDef["Host"].verify(launcher); } catch (e) { throw new Exception( 'Given launcher "' + launcher + '" was invalid: ' + e ); } return this.builder( info, { host: launcher }, streams, subs, controls, history ); } launcher(config) { return config.host; } }