diff --git a/Protest/Front/loader.js b/Protest/Front/loader.js index efb5c3db..4fc05c4b 100644 --- a/Protest/Front/loader.js +++ b/Protest/Front/loader.js @@ -104,7 +104,6 @@ const LOADER = { "dhcpdiscover.js", "ntpclient.js", "telnet.js", - "terminal.js", "wmi.js", "snmp.js", "speedtest.js", @@ -321,7 +320,6 @@ const LOADER = { case "NtpClient" : return new NtpClient(command.params); case "SiteCheck" : return new SiteCheck(command.params); case "Telnet" : return new Telnet(command.params); - case "Terminal" : return new Terminal(command.params); case "Wmi" : return new Wmi(command.params); case "Snmp" : return new Snmp(command.params); diff --git a/Protest/Front/telnet.js b/Protest/Front/telnet.js index 334176b2..2eb1f584 100644 --- a/Protest/Front/telnet.js +++ b/Protest/Front/telnet.js @@ -1,72 +1,68 @@ class Telnet extends Window { + static CURSOR_WIDTH = 8; + static CURSOR_HEIGHT = 18; + constructor(params) { super(); - this.params = params ? params : ""; - this.history = []; - this.ws = null; - this.last = null; + this.params = params ? params : {host:"", isAnsi:true, bell:true}; + + this.cursor = {x:0, y:0}; + this.chars = {}; + + this.savedCursorPos = null; + this.savedLine = null; + this.savedScreen = null; - let historyIndex = -1; + this.foreColor = null; + this.backColor = null; + this.isBold = false; + this.isDim = false; + this.isItalic = false; + this.isUnderline = false; + this.isBlinking = false; + this.isInverse = false; + this.isHidden = false; + this.isStrikethrough = false; + + this.ws = null; this.SetTitle("Telnet"); - this.SetIcon("mono/telnet.svg"); - - this.list = document.createElement("div"); - this.list.style.color = "#ccc"; - this.list.style.position = "absolute"; - this.list.style.overflowY = "auto"; - this.list.style.left = "0"; - this.list.style.right = "0"; - this.list.style.top = "0"; - this.list.style.bottom = "40px"; - this.list.style.margin = "8px 16px"; - this.list.style.fontFamily = "monospace"; - this.list.style.userSelect = "text"; - this.content.appendChild(this.list); - - this.inputBox = document.createElement("input"); - this.inputBox.type = "text"; - this.inputBox.style.position = "absolute"; - this.inputBox.style.left = "8px"; - this.inputBox.style.bottom = "8px"; - this.inputBox.style.width = "calc(100% - 16px)"; - this.inputBox.style.margin = "0"; - this.inputBox.style.border = "0"; - this.inputBox.style.boxSizing = "border-box"; - this.content.appendChild(this.inputBox); - - this.inputBox.onkeydown = event=> { - if (event.code === "Enter") { - this.Push(this.inputBox.value); - this.list.scrollTop = this.list.scrollHeight; - this.inputBox.value = ""; - event.preventDefault(); - } + this.SetIcon("mono/console.svg"); - if (event.code === "ArrowUp" || event.code === "ArrowDown") { - if (this.history.length == 0) return; + this.AddCssDependencies("terminal.css"); - if (event.code === "ArrowUp") historyIndex--; //up - if (event.code === "ArrowDown") historyIndex++; //down + this.SetupToolbar(); + this.connectButton = this.AddToolbarButton("Connect", "mono/connect.svg?light"); + this.optionsButton = this.AddToolbarButton("Options", "mono/wrench.svg?light"); + this.AddToolbarSeparator(); + this.sendKeyButton = this.AddToolbarButton("Send key", "mono/keyboard.svg?light"); + this.pasteButton = this.AddToolbarButton("Paste", "mono/clipboard.svg?light"); - if (historyIndex < 0) historyIndex = this.history.length - 1; - historyIndex %= this.history.length; - this.inputBox.value = this.history[historyIndex]; + this.content.tabIndex = 1; + this.content.classList.add("terminal-content"); - event.preventDefault(); - } - else if (event.code !== "ArrowLeft" && event.code !== "ArrowRight") { // not left nor rigth - historyIndex = -1; - } - }; + this.cursorElement = document.createElement("div"); + this.cursorElement.className = "terminal-cursor"; - this.defaultElement = this.inputBox; + this.statusBox = document.createElement("div"); + this.statusBox.className = "terminal-status-box"; + this.statusBox.textContent = "Connecting..."; - this.inputBox.onfocus = ()=> this.BringToFront(); - this.escAction = ()=> { this.inputBox.value = ""; }; + this.win.onclick = ()=> this.content.focus(); + this.content.onfocus = ()=> this.BringToFront(); + this.content.onkeydown = event=> this.Terminal_onkeydown(event); - this.ConnectDialog(this.params); + this.connectButton.onclick = ()=> this.ConnectDialog(this.params.host); + this.optionsButton.onclick = ()=> this.OptionsDialog(); + this.sendKeyButton.onclick = ()=> this.CustomKeyDialog(); + this.pasteButton.onclick = ()=> this.ClipboardDialog(); + + this.ConnectDialog(this.params.host, true); + + //preload icon: + const disconnectIcon = new Image(); + disconnectIcon.src = "mono/disconnect.svg"; } Close() { //overrides @@ -74,96 +70,217 @@ class Telnet extends Window { super.Close(); } - Push(command) { //overrides - if (command.length === 0) command = "\n"; + AfterResize() { //overrides + super.AfterResize(); + //TODO: + } - if (command === "!!" && this.history.length === 0) return false; + ConnectDialog(target="", isNew=false) { + const dialog = this.DialogBox("112px"); + if (dialog === null) return; - if (command === "!!") { - this.Push(this.history[this.history.length - 1]); - return false; - } + const okButton = dialog.okButton; + const cancelButton = dialog.cancelButton; + const innerBox = dialog.innerBox; - if (command.length > 0 && command !== "\r" && command !== "\n") - this.history.push(command); + innerBox.parentElement.style.maxWidth = "400px"; + innerBox.parentElement.parentElement.onclick = event=> { event.stopPropagation(); }; - this.PushLine(); - this.last.textContent = "> " + command; - this.last.style.color = "#fff"; - this.PushLine(); - this.list.scrollTop = this.list.scrollHeight; + innerBox.style.margin = "20px 8px 0 8px"; + innerBox.style.textAlign = "center"; - if (this.ws != null && this.ws.readyState === 1) {//ready - this.ws.send(command); + const hostLabel = document.createElement("div"); + hostLabel.style.display = "inline-block"; + hostLabel.style.minWidth = "50px"; + hostLabel.textContent = "Host:"; + const hostInput = document.createElement("input"); + hostInput.type = "text"; + hostInput.style.width = "calc(100% - 72px)"; + hostInput.value = target; + + innerBox.append(hostLabel, hostInput); + + okButton.onclick = ()=> { + dialog.Close(); + this.Connect(hostInput.value.trim()); + }; + + if (isNew) { + cancelButton.value = "Close"; + + cancelButton.onclick = ()=> { + dialog.Close(); + this.Close(); + }; } - else { - this.PushLine(); - this.last.textContent = "web socket error"; - this.last.style.color = "var(--theme-color)"; - this.PushLine(); - this.list.scrollTop = this.list.scrollHeight; - } - return true; + hostInput.onkeydown = event=> { + if (event.key === "Enter") { + dialog.okButton.click(); + } + }; + + setTimeout(()=> hostInput.focus(), 200); } - ConnectDialog(target = "") { - const dialog = this.DialogBox("128px"); + OptionsDialog() { + const dialog = this.DialogBox("168px"); if (dialog === null) return; const okButton = dialog.okButton; const cancelButton = dialog.cancelButton; - const buttonBox = dialog.buttonBox; const innerBox = dialog.innerBox; - innerBox.style.textAlign = "center"; - innerBox.style.padding = "20px 40px"; - - okButton.value = "Connect"; - if (target.length === 0) okButton.setAttribute("disabled", true); + innerBox.style.padding = "20px"; + innerBox.parentElement.style.maxWidth = "480px"; + innerBox.parentElement.parentElement.onclick = event=> { event.stopPropagation(); }; + + const ansiCheckbox = document.createElement("input"); + ansiCheckbox.type = "checkbox"; + ansiCheckbox.checked = this.params.isAnsi; + innerBox.appendChild(ansiCheckbox); + this.AddCheckBoxLabel(innerBox, ansiCheckbox, "Escape ANSI codes"); + + innerBox.appendChild(document.createElement("br")); + innerBox.appendChild(document.createElement("br")); + + const bellCheckbox = document.createElement("input"); + bellCheckbox.type = "checkbox"; + bellCheckbox.checked = this.params.bell; + innerBox.appendChild(bellCheckbox); + this.AddCheckBoxLabel(innerBox, bellCheckbox, "Play bell sound"); + + okButton.onclick = ()=> { + this.params.isAnsi = ansiCheckbox.checked; + this.params.bell = bellCheckbox.checked; + dialog.Close(); + }; + } - const hostLabel = document.createElement("div"); - hostLabel.textContent = "Remote host:"; - hostLabel.style.display = "inline-block"; - hostLabel.style.minWidth = "100px"; - innerBox.appendChild(hostLabel); + CustomKeyDialog() { + const dialog = this.DialogBox("180px"); + if (dialog === null) return; - const hostInput = document.createElement("input"); - hostInput.type = "text"; - hostInput.value = target; - hostInput.placeholder = "10.0.0.1:23"; - innerBox.appendChild(hostInput); + const okButton = dialog.okButton; + const innerBox = dialog.innerBox; - setTimeout(()=> hostInput.focus(), 50); + okButton.value = "Send"; + okButton.disabled = true; + + innerBox.style.padding = "20px"; + innerBox.parentElement.style.maxWidth = "500px"; + innerBox.parentElement.parentElement.onclick = event=> { event.stopPropagation(); }; + + const keyLabel = document.createElement("div"); + keyLabel.style.display = "inline-block"; + keyLabel.style.minWidth = "50px"; + keyLabel.textContent = "Key:"; + innerBox.appendChild(keyLabel); + + const keyInput = document.createElement("input"); + keyInput.type = "text"; + keyInput.setAttribute("maxlength", "1"); + keyInput.style.textAlign = "center"; + keyInput.style.width = "64px"; + innerBox.appendChild(keyInput); + + innerBox.appendChild(document.createElement("br")); + innerBox.appendChild(document.createElement("br")); + + const shiftCheckbox = document.createElement("input"); + shiftCheckbox.type = "checkbox"; + shiftCheckbox.checked = false; + innerBox.appendChild(shiftCheckbox); + this.AddCheckBoxLabel(innerBox, shiftCheckbox, "Shift").style.margin = "4px 1px"; + + const ctrlCheckbox = document.createElement("input"); + ctrlCheckbox.type = "checkbox"; + ctrlCheckbox.checked = false; + innerBox.appendChild(ctrlCheckbox); + this.AddCheckBoxLabel(innerBox, ctrlCheckbox, "Ctrl").style.margin = "4px 1px"; + + const altCheckbox = document.createElement("input"); + altCheckbox.type = "checkbox"; + altCheckbox.checked = false; + innerBox.appendChild(altCheckbox); + this.AddCheckBoxLabel(innerBox, altCheckbox, "Alt").style.margin = "4px 1px"; + + const altGrCheckbox = document.createElement("input"); + altGrCheckbox.type = "checkbox"; + altGrCheckbox.checked = false; + innerBox.appendChild(altGrCheckbox); + this.AddCheckBoxLabel(innerBox, altGrCheckbox, "Alt gr").style.margin = "4px 1px"; + + keyInput.onchange = keyInput.oninput = ()=> { + okButton.disabled = keyInput.value.length === 0; + }; - hostInput.oninput = hostInput.onchange = ()=> { - if (hostInput.value.length === 0) { - okButton.setAttribute("disabled", true); - } - else { - okButton.removeAttribute("disabled"); + keyInput.onkeydown = event=> { + if (event.key === "Enter" && !okButton.disabled) { + dialog.okButton.click(); } }; - hostInput.onkeydown = event=> { - if (hostInput.value.length === 0) return; - if (event.code === "Enter") { - this.Connect(hostInput.value); - cancelButton.onclick(); - } + okButton.onclick = ()=> { + dialog.Close(); + //TODO: Send key }; - okButton.addEventListener("click", ()=> { - this.Connect(hostInput.value); - }); + setTimeout(()=>{ keyInput.focus(); }, 200); + } - cancelButton.addEventListener("click", ()=> this.Close()); + async ClipboardDialog() { + const dialog = this.DialogBox("128px"); + if (dialog === null) return; + + const okButton = dialog.okButton; + const innerBox = dialog.innerBox; + + okButton.value = "Paste"; + okButton.disabled = true; + + innerBox.style.padding = "20px"; + innerBox.parentElement.style.maxWidth = "560px"; + innerBox.parentElement.parentElement.onclick = event=> { event.stopPropagation(); }; + + const keyText = document.createElement("input"); + keyText.type = "text"; + keyText.style.width = "calc(100% - 8px)"; + keyText.style.boxSizing = "border-box"; + innerBox.appendChild(keyText); + + try { + keyText.value = await navigator.clipboard.readText(); + okButton.disabled = keyText.value.length === 0; + } + catch (ex) { + dialog.Close(); + setTimeout(()=>{ this.ConfirmBox(ex, true, "mono/error.svg"); }, 250); + } + + keyText.onchange = keyText.oninput = ()=> { + okButton.disabled = keyText.value.length === 0; + }; + + okButton.onclick = ()=> { + dialog.Close(); + if (this.ws === null || this.ws.readyState != 1) { + return; + } + this.ws.send(keyText.value); + }; + + setTimeout(()=>{ keyText.focus(); }, 200); } Connect(target) { - this.params = target; - this.inputBox.focus(); + this.params.host = target; + + this.statusBox.style.display = "initial"; + this.statusBox.style.backgroundImage = "url(mono/connect.svg)"; + this.statusBox.textContent = "Connecting..."; + this.content.appendChild(this.statusBox); let server = window.location.href; server = server.replace("https://", ""); @@ -174,187 +291,642 @@ class Telnet extends Window { try { this.ws.close(); } - catch {}; + catch {} } + try { this.ws = new WebSocket((KEEP.isSecure ? "wss://" : "ws://") + server + "/ws/telnet"); } catch {} - this.PushLine(); - this.ws.onopen = ()=> { + this.connectButton.disabled = true; this.ws.send(target); }; this.ws.onclose = ()=> { - const error_message = this.PushLine(); - error_message.id = "self_destruct"; - error_message.textContent = "Connection is closed. Click to reconnect"; - error_message.style.color = "var(--clr-accent)"; - error_message.style.backgroundColor = "rgb(48,48,48)"; - error_message.style.cursor = "pointer"; - error_message.style.textAlign = "center"; - error_message.style.borderRadius = "4px"; - error_message.style.margin = "8px auto"; - error_message.style.padding = "8px"; - error_message.style.maxWidth = "320px"; - error_message.style.animation = "fade-in .4s 1"; - - error_message.onclick = ()=> { - error_message.onclick = ()=> {}; - error_message.style.cursor = ""; - error_message.textContent = "tcp connection has been terminated"; - this.list.appendChild(document.createElement("hr")); - this.PushLine(); - this.Connect(target); - this.list.scrollTop = this.list.scrollHeight; - }; - this.list.scrollTop = this.list.scrollHeight; + this.statusBox.style.display = "initial"; + this.statusBox.style.backgroundImage = "url(mono/disconnect.svg)"; + this.statusBox.textContent = "Connection closed"; + this.content.appendChild(this.statusBox); + + this.connectButton.disabled = false; }; - let front = "#ccc"; - let back = "transparent"; - let bold = false; - let underline = false; - - this.ws.onmessage = event=> { - let payload = event.data; - let line = payload.split("\n"); - - for (let i = 0; i < line.length; i++) { - let s = line[i].indexOf(String.fromCharCode(27)); - - if (s > 0) { //styled - - let split = line[i].split(String.fromCharCode(27)); //esc - for (let j = 0; j < split.length; j++) { - - let e = split[j].indexOf("m"); //ansi stop - if (e == -1) e = split[j].indexOf("J"); //clear screen - if (e == -1) e = split[j].indexOf("K"); //clear line - - if (e == -1) e = split[j].indexOf("A"); //move cursor | - if (e == -1) e = split[j].indexOf("B"); //move cursor | - if (e == -1) e = split[j].indexOf("C"); //move cursor | - if (e == -1) e = split[j].indexOf("D"); //move cursor | cursor navigation - if (e == -1) e = split[j].indexOf("E"); //move cursor | is not supported - if (e == -1) e = split[j].indexOf("F"); //move cursor | - if (e == -1) e = split[j].indexOf("G"); //move cursor | - if (e == -1) e = split[j].indexOf("H"); //move cursor | - - if (e == -1) e = split[j].indexOf(" "); - - let ansi = split[j].substring(0, e+1); - switch (ansi) { - case "[0m": - front = "#ccc"; - back = "transparent"; - bold = false; - underline = false; - break; - - case "[1m": bold = true; break; - case "[4m": underline = true; break; - - case "[7m": - front = "#222"; - back = "#ccc"; - break; - - case "[30m": front = "#000"; break; - case "[31m": front = "#d00"; break; - case "[32m": front = "#0d0"; break; - case "[33m": front = "#0dd"; break; - case "[34m": front = "#00d"; break; - case "[35m": front = "#d0d"; break; - case "[36m": front = "#0dd"; break; - case "[37m": front = "#ddd"; break; - - case "[30;1m": front = "#222"; bold = true; break; - case "[31;1m": front = "#f22"; bold = true; break; - case "[32;1m": front = "#2f2"; bold = true; break; - case "[33;1m": front = "#2ff"; bold = true; break; - case "[34;1m": front = "#22f"; bold = true; break; - case "[35;1m": front = "#f2f"; bold = true; break; - case "[36;1m": front = "#2ff"; bold = true; break; - case "[37;1m": front = "#fff"; bold = true; break; - - case "[40m": back = "#000"; break; - case "[41m": back = "#d00"; break; - case "[42m": back = "#0d0"; break; - case "[43m": back = "#0dd"; break; - case "[44m": back = "#00d"; break; - case "[45m": back = "#d0d"; break; - case "[46m": back = "#0dd"; break; - case "[47m": back = "#ddd"; break; - - case "[40;1m": back = "#000"; bold = true; break; - case "[41;1m": back = "#d00"; bold = true; break; - case "[42;1m": back = "#0d0"; bold = true; break; - case "[43;1m": back = "#0dd"; bold = true; break; - case "[44;1m": back = "#00d"; bold = true; break; - case "[45;1m": back = "#d0d"; bold = true; break; - case "[46;1m": back = "#0dd"; bold = true; break; - case "[47;1m": back = "#ddd"; bold = true; break; - - case "[0J": case "[1J": case "[2J": - this.PushLine(); - this.last.style.height = this.content.clientHeight; - this.PushLine(); - break; - - case "[0K": case "[1K": case "[2K": - this.last.textContent = ""; - break; - } - - this.PushText(split[j].replace(ansi, ""), front, back, bold, underline); - } + this.ws.onmessage = e=> { + let json = JSON.parse(e.data); + if (json.connected) { + this.SetTitle(`Telnet - ${target}`); + this.statusBox.style.display = "none"; + this.ws.onmessage = event=> this.HandleMessage(event.data); + + this.content.appendChild(this.cursorElement); + this.content.focus(); + } + else if (json.error) { + setTimeout(()=>{ this.ConfirmBox(json.error, true, "mono/error.svg"); }, 200); + } + }; + } + + Terminal_onkeydown(event) { + event.preventDefault(); + + if (this.ws === null || this.ws.readyState != 1) { + return; + } + + if (event.ctrlKey && event.key.length === 1) { + switch (event.code) { + case "KeyA": this.ws.send("\x01"); return; + case "KeyB": this.ws.send("\x02"); return; + case "KeyC": this.ws.send("\x03"); return; + case "KeyD": this.ws.send("\x04"); return; + case "KeyE": this.ws.send("\x05"); return; + case "KeyF": this.ws.send("\x06"); return; + case "KeyG": this.ws.send("\x07"); return; + case "KeyH": this.ws.send("\x08"); return; + case "KeyI": this.ws.send("\x09"); return; + case "KeyJ": this.ws.send("\x10"); return; + case "KeyK": this.ws.send("\x11"); return; + case "KeyL": this.ws.send("\x12"); return; + case "KeyM": this.ws.send("\x13"); return; + case "KeyN": this.ws.send("\x14"); return; + case "KeyO": this.ws.send("\x15"); return; + case "KeyP": this.ws.send("\x16"); return; + case "KeyQ": this.ws.send("\x17"); return; + case "KeyR": this.ws.send("\x18"); return; + case "KeyS": this.ws.send("\x19"); return; + case "KeyT": this.ws.send("\x20"); return; + case "KeyU": this.ws.send("\x21"); return; + case "KeyV": this.ws.send("\x22"); return; + case "KeyW": this.ws.send("\x23"); return; + case "KeyX": this.ws.send("\x24"); return; + case "KeyY": this.ws.send("\x25"); return; + case "KeyZ": this.ws.send("\x26"); return; + } + } + else if (event.ctrlKey) { + //TODO: ctrl+key + } + + if (event.key.length === 1) { + this.ws.send(event.key); + return; + } + + switch(event.key) { + case "Enter" : this.ws.send("\r\n"); return; + case "Tab" : this.ws.send("\t"); return; + case "Backspace" : this.ws.send("\x08"); return; + case "Delete" : this.ws.send("\x1b[3~"); return; + case "ArrowLeft" : this.ws.send("\x1b[D"); return; + case "ArrowRight": this.ws.send("\x1b[C"); return; + case "ArrowUp" : this.ws.send("\x1b[A"); return; + case "ArrowDown" : this.ws.send("\x1b[B"); return; + case "Home" : this.ws.send("\x1b[H"); return; + case "End" : this.ws.send("\x1b[F"); return; + } + } + + HandleMessage(data) { + for (let i=0; i= data.length) return 2; + + let symbol = null; + let values = []; + let command = null; + + let i = index + 2; + while (i < data.length) { + if (this.IsLetter(data, i)) { //command + command = data[i++]; + break; + } + + if (!isNaN(data[i])) { //number + let n = parseInt(data[i]); + while (!isNaN(data[++i]) && i < data.length) { + n *= 10; + n += parseInt(data[i]); + } + values.push(n); + continue; + } + + switch (data[i]) { + case ";": symbol = ";"; break; //separator + case "=": symbol = "="; break; //screen mode + case "?": symbol = "?"; break; //private modes + default: + console.warn("Unknown token: " + data[i]); + break; + } + i++; + } + + switch (command) { + case "A": //cursor up + this.cursor.y = Math.max(0, this.cursor.y - values[0] ?? 1); + break; + + case "B": //cursor down + this.cursor.y += values[0] ?? 1; + break; + + case "C": //cursor right + this.cursor.x += values[0] ?? 1; + break; + + case "D": //cursor left + this.cursor.x = Math.max(0, this.cursor.x - values[0] ?? 1); + break; + + case "E": //cursor to beginning of next line, n lines down + this.cursor.x = 0; + this.cursor.y += values[0] ?? 1; + break; + + case "F": //cursor to beginning of previous line, n lines up + this.cursor.x = 0; + this.cursor.y = Math.max(0, this.cursor.y - values[0] ?? 1); + + case "G": //cursor to column n + if (values.length > 0) break; + this.cursor.x = values[0]; + break; + + case "f": + case "H": //cursor to home position (0,0) + if (values.length === 0) { + this.cursor.x = 0; + this.cursor.y = 0; + return 3; + } + else if (values.length > 1) { + this.cursor.x = values[0]; + this.cursor.y = values[1]; + } + break; + + case "J": + if (values.length === 0) { //same as J0 + this.EraseFromCursorToEndOfScreen(); + return 3; + } + if (values[0] === 0) { + this.EraseFromCursorToEndOfScreen(); + return 4; + } + else if (values[0] === 1) { + this.EraseFromCursorToBeginningOfScreen(); + return 4; + } + else if (values[0] === 2) { + this.ClearScreen(); + return 4; + } + else if (values[0] === 3) { + //TODO: erase saved lines + console.warn("Unknown CSI: 3J"); + return 4; + } + break; + + case "K": + if (values.length === 0) { //same as K0 + this.EraseLineFromCursorToEnd(); + return 3; + } + if (values[0] === 0) { + this.EraseLineFromCursorToEnd(); + return 4; + } + if (values[0] === 1) { + this.EraseLineFromBeginningToCursor(); + return 4; + } + if (values[0] === 2) { + this.ClearLine(); + return 4; + } + break; + + case "P": //delete n chars + if (values.length === 0) break; + + //find last character in current line + let x; + for (x=this.cursor.x; x 0 && - this.last.textContent.trim() === this.history[this.history.length - 1].trim()) { - this.last.textContent = ""; + case 48: //set background color + if (values.length < 3) break; + + if (values[1] === 5) { //id color + if (values.length < 3) break; + this.backColor = this.MapColorId(values[2]); + } + else if (values[1] === 2) { //rgb color + if (values.length < 6) break; + this.backColor = `rgb(${values[2]},${values[3]},${values[4]})`; } else { - this.PushLine(); + console.warn(`Unknown graphics mode: 48;${values[1]}`); } + + default: + console.warn(`Unknown graphics mode: ${values[p]}`); } } - this.list.scrollTop = this.list.scrollHeight; - }; + break; + + default: + console.warn(`Unknown CSI command: ${symbol ?? ""}${values.join(";")}${command}`); + break; + } + + return i - index + 1; + } + + HandleDCS(data, index) { //Device Control String + if (index >= data.length) return 2; + + console.warn("Unknown DCS: " + data[index+2]); + return 2; + } + + HandleOSC(data, index) { //Operating System Command + if (index >= data.length) return 2; + + console.warn("Unknown OCS: " + data[index+2]); + return 2; + } + + MapColorId(id) { + switch (id) { + case 0: return "#000"; + case 1: return "#800"; + case 2: return "#080"; + case 3: return "#880"; + case 4: return "#008"; + case 5: return "#808"; + case 6: return "#088"; + case 7: return "#ccc"; + + case 8: return "#888"; + case 9: return "#f00"; + case 10: return "#0f0"; + case 11: return "#ff0"; + case 12: return "#00f"; + case 13: return "#f0f"; + case 14: return "#0ff"; + case 15: return "#fff"; + } + + if (id > 231) { + let hex = (8 + (id - 232) * 10).toString(16).padStart(2, "0"); + return `#${hex.repeat(3)}`; + } + + let v = id - 16; + let r = Math.floor(v / 36) * 51; + let g = Math.floor((v % 36) / 6) * 51; + let b = (v % 6) * 51; + + return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; + } + + IsLetter(string, index) { + const code = string.charCodeAt(index); + if (code > 64 && code < 91) return true; + if (code > 96 && code < 123) return true; + return false; + } + + ClearLine() { + const w = this.GetScreenWidth(); + for (let i=0; i c) continue; + const key = `${x},${y}`; + if (!this.chars[key]) continue; + this.content.removeChild(this.chars[key]); + delete this.chars[key]; + } + } + } + + ClearScreen() { //2J + this.chars = {}; + this.content.innerHTML = ""; + this.content.appendChild(this.cursorElement); + } - //this.ws.onerror = error=> console.error(error); + EraseLineFromCursorToEnd() { //0K + const w = this.GetScreenWidth(); + for (let i=this.cursor.x; i{ oscillator.stop() }, 150); } } \ No newline at end of file diff --git a/Protest/Front/terminal.css b/Protest/Front/terminal.css index 0ea3551e..2c8b3ee0 100644 --- a/Protest/Front/terminal.css +++ b/Protest/Front/terminal.css @@ -1,6 +1,7 @@ .terminal-content { font-family: consolas, monospace; margin: 20px; + overflow-y: scroll; -webkit-user-select: text; user-select: text; } @@ -11,6 +12,29 @@ font-size: 15px; } +.terminal-status-box { + display: none; + position: sticky; + top: 20px; + left: calc(50% - 110px); + width: 200px; + padding: 20px 20px 20px 64px; + text-align: center; + z-index: 1; + border-radius: 4px; + box-shadow: var(--clr-dark) 0 0 4px; + font-family: var(--global-font-family); + font-weight: 800; + white-space: nowrap; + color: var(--clr-dark); + background-color: var(--clr-pane); + background-image: url(mono/connect.svg); + background-size: 48px 48px; + background-position: 8px 50%; + background-repeat: no-repeat; + -webkit-user-select: none; user-select: none; +} + .terminal-cursor { position: absolute; top: 0; diff --git a/Protest/Front/terminal.js b/Protest/Front/terminal.js deleted file mode 100644 index 2f883f16..00000000 --- a/Protest/Front/terminal.js +++ /dev/null @@ -1,948 +0,0 @@ -class Terminal extends Window { - static CURSOR_WIDTH = 8; - static CURSOR_HEIGHT = 18; - - constructor(params) { - super(); - - this.params = params ? params : {host:"", isAnsi:true, bell:true}; - - this.cursor = {x:0, y:0}; - this.chars = {}; - - this.savedCursorPos = null; - this.savedLine = null; - this.savedScreen = null; - - this.foreColor = null; - this.backColor = null; - this.isBold = false; - this.isDim = false; - this.isItalic = false; - this.isUnderline = false; - this.isBlinking = false; - this.isInverse = false; - this.isHidden = false; - this.isStrikethrough = false; - - this.ws = null; - - this.SetTitle("Terminal"); - this.SetIcon("mono/console.svg"); - - this.AddCssDependencies("terminal.css"); - - this.SetupToolbar(); - this.connectButton = this.AddToolbarButton("Connect", "mono/connect.svg?light"); - this.optionsButton = this.AddToolbarButton("Options", "mono/wrench.svg?light"); - this.AddToolbarSeparator(); - this.sendKeyButton = this.AddToolbarButton("Send key", "mono/keyboard.svg?light"); - this.pasteButton = this.AddToolbarButton("Paste", "mono/clipboard.svg?light"); - - this.content.tabIndex = 1; - this.content.classList.add("terminal-content"); - - this.cursorElement = document.createElement("div"); - this.cursorElement.className = "terminal-cursor"; - - this.statusBox = document.createElement("div"); - this.statusBox.style.display = "none"; - this.statusBox.style.position = "absolute"; - this.statusBox.style.top = "0"; - this.statusBox.style.left = "calc(50% - 48px)"; - this.statusBox.style.zIndex = "1"; - this.statusBox.style.width = "168px"; - this.statusBox.style.height = "120px"; - this.statusBox.style.borderRadius = "4px"; - this.statusBox.style.boxShadow = "var(--clr-dark) 0 0 4px"; - this.statusBox.style.fontFamily = "var(--global-font-family)"; - this.statusBox.style.fontWeight = "800"; - this.statusBox.style.lineHeight = "192px"; - this.statusBox.style.whiteSpace = "nowrap"; - this.statusBox.style.color = "var(--clr-dark)"; - this.statusBox.style.backgroundColor = "var(--clr-pane)"; - this.statusBox.style.backgroundImage = "url(mono/connect.svg)"; - this.statusBox.style.backgroundSize = "64px 64px"; - this.statusBox.style.backgroundPosition = "50% 16px"; - this.statusBox.style.backgroundRepeat = "no-repeat"; - this.statusBox.style.textAlign = "center"; - this.statusBox.textContent = "Connecting..."; - - this.win.onclick = ()=> this.content.focus(); - this.content.onfocus = ()=> this.BringToFront(); - this.content.onkeydown = event=> this.Terminal_onkeydown(event); - - this.connectButton.onclick = ()=> this.ConnectDialog(this.params.host); - this.optionsButton.onclick = ()=> this.OptionsDialog(); - this.sendKeyButton.onclick = ()=> this.CustomKeyDialog(); - this.pasteButton.onclick = ()=> this.ClipboardDialog(); - - this.ConnectDialog(this.params.host, true); - - //preload icon: - const disconnectIcon = new Image(); - disconnectIcon.src = "mono/disconnect.svg"; - } - - Close() { //overrides - if (this.ws != null) this.ws.close(); - super.Close(); - } - - AfterResize() { //overrides - super.AfterResize(); - //TODO: - } - - ConnectDialog(target="", isNew=false) { - const dialog = this.DialogBox("112px"); - if (dialog === null) return; - - const okButton = dialog.okButton; - const cancelButton = dialog.cancelButton; - const innerBox = dialog.innerBox; - - innerBox.parentElement.style.maxWidth = "400px"; - innerBox.parentElement.parentElement.onclick = event=> { event.stopPropagation(); }; - - innerBox.style.margin = "20px 8px 0 8px"; - innerBox.style.textAlign = "center"; - - const hostLabel = document.createElement("div"); - hostLabel.style.display = "inline-block"; - hostLabel.style.minWidth = "50px"; - hostLabel.textContent = "Host:"; - - const hostInput = document.createElement("input"); - hostInput.type = "text"; - hostInput.style.width = "calc(100% - 72px)"; - hostInput.value = target; - - innerBox.append(hostLabel, hostInput); - - okButton.onclick = ()=> { - dialog.Close(); - this.Connect(hostInput.value.trim()); - }; - - if (isNew) { - cancelButton.value = "Close"; - - cancelButton.onclick = ()=> { - dialog.Close(); - this.Close(); - }; - } - - hostInput.onkeydown = event=> { - if (event.key === "Enter") { - dialog.okButton.click(); - } - }; - - setTimeout(()=> hostInput.focus(), 200); - } - - OptionsDialog() { - const dialog = this.DialogBox("168px"); - if (dialog === null) return; - - const okButton = dialog.okButton; - const cancelButton = dialog.cancelButton; - const innerBox = dialog.innerBox; - - innerBox.style.padding = "20px"; - innerBox.parentElement.style.maxWidth = "480px"; - innerBox.parentElement.parentElement.onclick = event=> { event.stopPropagation(); }; - - const ansiCheckbox = document.createElement("input"); - ansiCheckbox.type = "checkbox"; - ansiCheckbox.checked = this.params.isAnsi; - innerBox.appendChild(ansiCheckbox); - this.AddCheckBoxLabel(innerBox, ansiCheckbox, "Escape ANSI codes"); - - innerBox.appendChild(document.createElement("br")); - innerBox.appendChild(document.createElement("br")); - - const bellCheckbox = document.createElement("input"); - bellCheckbox.type = "checkbox"; - bellCheckbox.checked = this.params.bell; - innerBox.appendChild(bellCheckbox); - this.AddCheckBoxLabel(innerBox, bellCheckbox, "Play bell sound"); - - okButton.onclick = ()=> { - this.params.isAnsi = ansiCheckbox.checked; - this.params.bell = bellCheckbox.checked; - dialog.Close(); - }; - } - - CustomKeyDialog() { - const dialog = this.DialogBox("180px"); - if (dialog === null) return; - - const okButton = dialog.okButton; - const innerBox = dialog.innerBox; - - okButton.value = "Send"; - okButton.disabled = true; - - innerBox.style.padding = "20px"; - innerBox.parentElement.style.maxWidth = "500px"; - innerBox.parentElement.parentElement.onclick = event=> { event.stopPropagation(); }; - - const keyLabel = document.createElement("div"); - keyLabel.style.display = "inline-block"; - keyLabel.style.minWidth = "50px"; - keyLabel.textContent = "Key:"; - innerBox.appendChild(keyLabel); - - const keyInput = document.createElement("input"); - keyInput.type = "text"; - keyInput.setAttribute("maxlength", "1"); - keyInput.style.textAlign = "center"; - keyInput.style.width = "64px"; - innerBox.appendChild(keyInput); - - innerBox.appendChild(document.createElement("br")); - innerBox.appendChild(document.createElement("br")); - - const shiftCheckbox = document.createElement("input"); - shiftCheckbox.type = "checkbox"; - shiftCheckbox.checked = false; - innerBox.appendChild(shiftCheckbox); - this.AddCheckBoxLabel(innerBox, shiftCheckbox, "Shift").style.margin = "4px 1px"; - - const ctrlCheckbox = document.createElement("input"); - ctrlCheckbox.type = "checkbox"; - ctrlCheckbox.checked = false; - innerBox.appendChild(ctrlCheckbox); - this.AddCheckBoxLabel(innerBox, ctrlCheckbox, "Ctrl").style.margin = "4px 1px"; - - const altCheckbox = document.createElement("input"); - altCheckbox.type = "checkbox"; - altCheckbox.checked = false; - innerBox.appendChild(altCheckbox); - this.AddCheckBoxLabel(innerBox, altCheckbox, "Alt").style.margin = "4px 1px"; - - const altGrCheckbox = document.createElement("input"); - altGrCheckbox.type = "checkbox"; - altGrCheckbox.checked = false; - innerBox.appendChild(altGrCheckbox); - this.AddCheckBoxLabel(innerBox, altGrCheckbox, "Alt gr").style.margin = "4px 1px"; - - keyInput.onchange = keyInput.oninput = ()=> { - okButton.disabled = keyInput.value.length === 0; - }; - - keyInput.onkeydown = event=> { - if (event.key === "Enter" && !okButton.disabled) { - dialog.okButton.click(); - } - }; - - okButton.onclick = ()=> { - dialog.Close(); - //TODO: Send key - }; - - setTimeout(()=>{ keyInput.focus(); }, 200); - } - - async ClipboardDialog() { - const dialog = this.DialogBox("128px"); - if (dialog === null) return; - - const okButton = dialog.okButton; - const innerBox = dialog.innerBox; - - okButton.value = "Paste"; - okButton.disabled = true; - - innerBox.style.padding = "20px"; - innerBox.parentElement.style.maxWidth = "560px"; - innerBox.parentElement.parentElement.onclick = event=> { event.stopPropagation(); }; - - const keyText = document.createElement("input"); - keyText.type = "text"; - keyText.style.width = "calc(100% - 8px)"; - keyText.style.boxSizing = "border-box"; - innerBox.appendChild(keyText); - - try { - keyText.value = await navigator.clipboard.readText(); - okButton.disabled = keyText.value.length === 0; - } - catch (ex) { - dialog.Close(); - setTimeout(()=>{ this.ConfirmBox(ex, true, "mono/error.svg"); }, 250); - } - - keyText.onchange = keyText.oninput = ()=> { - okButton.disabled = keyText.value.length === 0; - }; - - okButton.onclick = ()=> { - dialog.Close(); - if (this.ws === null || this.ws.readyState != 1) { - return; - } - this.ws.send(keyText.value); - }; - - setTimeout(()=>{ keyText.focus(); }, 200); - } - - Connect(target) { - this.params.host = target; - - this.statusBox.style.display = "initial"; - this.statusBox.style.backgroundImage = "url(mono/connect.svg)"; - this.statusBox.textContent = "Connecting..."; - this.content.appendChild(this.statusBox); - - let server = window.location.href; - server = server.replace("https://", ""); - server = server.replace("http://", ""); - if (server.indexOf("/") > 0) server = server.substring(0, server.indexOf("/")); - - if (this.ws != null) { - try { - this.ws.close(); - } - catch {} - } - - try { - this.ws = new WebSocket((KEEP.isSecure ? "wss://" : "ws://") + server + "/ws/telnet2"); - } - catch {} - - this.ws.onopen = ()=> { - this.connectButton.disabled = true; - this.ws.send(target); - }; - - this.ws.onclose = ()=> { - this.statusBox.style.display = "initial"; - this.statusBox.style.backgroundImage = "url(mono/disconnect.svg)"; - this.statusBox.textContent = "Connection closed"; - this.content.appendChild(this.statusBox); - - this.connectButton.disabled = false; - }; - - this.ws.onmessage = e=> { - let json = JSON.parse(e.data); - if (json.connected) { - this.statusBox.style.display = "none"; - this.ws.onmessage = event=> this.HandleMessage(event.data); - - this.content.appendChild(this.cursorElement); - this.content.focus(); - } - else if (json.error) { - setTimeout(()=>{ this.ConfirmBox(json.error, true, "mono/error.svg"); }, 200); - } - }; - } - - Terminal_onkeydown(event) { - event.preventDefault(); - - if (this.ws === null || this.ws.readyState != 1) { - return; - } - - if (event.ctrlKey && event.key.length === 1) { - switch (event.code) { - case "KeyA": this.ws.send("\x01"); return; - case "KeyB": this.ws.send("\x02"); return; - case "KeyC": this.ws.send("\x03"); return; - case "KeyD": this.ws.send("\x04"); return; - case "KeyE": this.ws.send("\x05"); return; - case "KeyF": this.ws.send("\x06"); return; - case "KeyG": this.ws.send("\x07"); return; - case "KeyH": this.ws.send("\x08"); return; - case "KeyI": this.ws.send("\x09"); return; - case "KeyJ": this.ws.send("\x10"); return; - case "KeyK": this.ws.send("\x11"); return; - case "KeyL": this.ws.send("\x12"); return; - case "KeyM": this.ws.send("\x13"); return; - case "KeyN": this.ws.send("\x14"); return; - case "KeyO": this.ws.send("\x15"); return; - case "KeyP": this.ws.send("\x16"); return; - case "KeyQ": this.ws.send("\x17"); return; - case "KeyR": this.ws.send("\x18"); return; - case "KeyS": this.ws.send("\x19"); return; - case "KeyT": this.ws.send("\x20"); return; - case "KeyU": this.ws.send("\x21"); return; - case "KeyV": this.ws.send("\x22"); return; - case "KeyW": this.ws.send("\x23"); return; - case "KeyX": this.ws.send("\x24"); return; - case "KeyY": this.ws.send("\x25"); return; - case "KeyZ": this.ws.send("\x26"); return; - } - } - else if (event.ctrlKey) { - //TODO: ctrl+key - } - - if (event.key.length === 1) { - this.ws.send(event.key); - return; - } - - switch(event.key) { - case "Enter" : this.ws.send("\r\n"); return; - case "Tab" : this.ws.send("\t"); return; - case "Backspace" : this.ws.send("\x08"); return; - case "Delete" : this.ws.send("\x1b[3~"); return; - case "ArrowLeft" : this.ws.send("\x1b[D"); return; - case "ArrowRight": this.ws.send("\x1b[C"); return; - case "ArrowUp" : this.ws.send("\x1b[A"); return; - case "ArrowDown" : this.ws.send("\x1b[B"); return; - case "Home" : this.ws.send("\x1b[H"); return; - case "End" : this.ws.send("\x1b[F"); return; - } - } - - HandleMessage(data) { - for (let i=0; i= data.length) return 2; - - let symbol = null; - let values = []; - let command = null; - - let i = index + 2; - while (i < data.length) { - if (this.IsLetter(data, i)) { //command - command = data[i++]; - break; - } - - if (!isNaN(data[i])) { //number - let n = parseInt(data[i]); - while (!isNaN(data[++i]) && i < data.length) { - n *= 10; - n += parseInt(data[i]); - } - values.push(n); - continue; - } - - switch (data[i]) { - case ";": symbol = ";"; break; //separator - case "=": symbol = "="; break; //screen mode - case "?": symbol = "?"; break; //private modes - default: - console.warn("Unknown token: " + data[i]); - break; - } - i++; - } - - switch (command) { - case "A": //cursor up - this.cursor.y = Math.max(0, this.cursor.y - values[0] ?? 1); - break; - - case "B": //cursor down - this.cursor.y += values[0] ?? 1; - break; - - case "C": //cursor right - this.cursor.x += values[0] ?? 1; - break; - - case "D": //cursor left - this.cursor.x = Math.max(0, this.cursor.x - values[0] ?? 1); - break; - - case "E": //cursor to beginning of next line, n lines down - this.cursor.x = 0; - this.cursor.y += values[0] ?? 1; - break; - - case "F": //cursor to beginning of previous line, n lines up - this.cursor.x = 0; - this.cursor.y = Math.max(0, this.cursor.y - values[0] ?? 1); - - case "G": //cursor to column n - if (values.length > 0) break; - this.cursor.x = values[0]; - break; - - case "f": - case "H": //cursor to home position (0,0) - if (values.length === 0) { - this.cursor.x = 0; - this.cursor.y = 0; - return 3; - } - else if (values.length > 1) { - this.cursor.x = values[0]; - this.cursor.y = values[1]; - } - break; - - case "J": - if (values.length === 0) { //same as J0 - this.EraseFromCursorToEndOfScreen(); - return 3; - } - if (values[0] === 0) { - this.EraseFromCursorToEndOfScreen(); - return 4; - } - else if (values[0] === 1) { - this.EraseFromCursorToBeginningOfScreen(); - return 4; - } - else if (values[0] === 2) { - this.ClearScreen(); - return 4; - } - else if (values[0] === 3) { - //TODO: erase saved lines - console.warn("Unknown CSI: 3J"); - return 4; - } - break; - - case "K": - if (values.length === 0) { //same as K0 - this.EraseLineFromCursorToEnd(); - return 3; - } - if (values[0] === 0) { - this.EraseLineFromCursorToEnd(); - return 4; - } - if (values[0] === 1) { - this.EraseLineFromBeginningToCursor(); - return 4; - } - if (values[0] === 2) { - this.ClearLine(); - return 4; - } - break; - - case "P": //delete n chars - if (values.length === 0) break; - - //find last character in current line - let x; - for (x=this.cursor.x; x= data.length) return 2; - - console.warn("Unknown DCS: " + data[index+2]); - return 2; - } - - HandleOSC(data, index) { //Operating System Command - if (index >= data.length) return 2; - - console.warn("Unknown OCS: " + data[index+2]); - return 2; - } - - MapColorId(id) { - switch (id) { - case 0: return "#000"; - case 1: return "#800"; - case 2: return "#080"; - case 3: return "#880"; - case 4: return "#008"; - case 5: return "#808"; - case 6: return "#088"; - case 7: return "#ccc"; - - case 8: return "#888"; - case 9: return "#f00"; - case 10: return "#0f0"; - case 11: return "#ff0"; - case 12: return "#00f"; - case 13: return "#f0f"; - case 14: return "#0ff"; - case 15: return "#fff"; - } - - if (id > 231) { - let hex = (8 + (id - 232) * 10).toString(16).padStart(2, "0"); - return `#${hex.repeat(3)}`; - } - - let v = id - 16; - let r = Math.floor(v / 36) * 51; - let g = Math.floor((v % 36) / 6) * 51; - let b = (v % 6) * 51; - - return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; - } - - IsLetter(string, index) { - const code = string.charCodeAt(index); - if (code > 64 && code < 91) return true; - if (code > 96 && code < 123) return true; - return false; - } - - ClearLine() { - const w = this.GetScreenWidth(); - for (let i=0; i c) continue; - const key = `${x},${y}`; - if (!this.chars[key]) continue; - this.content.removeChild(this.chars[key]); - delete this.chars[key]; - } - } - } - - ClearScreen() { //2J - this.chars = {}; - this.content.innerHTML = ""; - this.content.appendChild(this.cursorElement); - } - - EraseLineFromCursorToEnd() { //0K - const w = this.GetScreenWidth(); - for (let i=this.cursor.x; i{ oscillator.stop() }, 150); - } -} \ No newline at end of file diff --git a/Protest/Front/ui.js b/Protest/Front/ui.js index ee908c69..679e8381 100644 --- a/Protest/Front/ui.js +++ b/Protest/Front/ui.js @@ -301,7 +301,7 @@ const MENU = { { t:"Watchdog", i:"mono/watchdog.svg?light", g:"documentation", h:false, f:params=> new Watchdog(params), k:"" }, { t:"Team chat", i:"mono/chat.svg?light", g:"documentation", h:false, f:params=> new Chat(), k:"messages" }, - { t:"Telnet", i:"mono/telnet.svg?light", g:"tools", h:true, f:params=> new Telnet(params) }, + { t:"Telnet", i:"mono/telnet.svg?light", g:"tools", h:true, f:params=> new Telnet({host:"", isAnsi:true, bell:true}) }, //{ t:"Secure shell", i:"mono/ssh.svg?light", g:"tools", h:true, f:params=> {} }, { t:"WMI client", i:"mono/wmi.svg?light", g:"tools", h:false, f:params=> new Wmi(params), k:"windows management instrumentation viewer" }, { t:"SNMP polling", i:"mono/snmp.svg?light", g:"tools", h:false, f:params=> new Snmp(params) }, diff --git a/Protest/Http/Listener.cs b/Protest/Http/Listener.cs index eb94d6bf..a5929f2b 100644 --- a/Protest/Http/Listener.cs +++ b/Protest/Http/Listener.cs @@ -444,10 +444,6 @@ private static bool WebSocketHandler(HttpListenerContext ctx) { Protocols.Telnet.WebSocketHandler(ctx); return true; - case "/ws/telnet2": - Protocols.Telnet.WebSocketHandler2(ctx); - return true; - case "/ws/ssh": //Ssh.WebSocketHandler(ctx); break; diff --git a/Protest/Protocols/Telnet.cs b/Protest/Protocols/Telnet.cs index a90a9666..44535f87 100644 --- a/Protest/Protocols/Telnet.cs +++ b/Protest/Protocols/Telnet.cs @@ -21,7 +21,7 @@ private static async Task WsWriteText(WebSocket ws, byte[] data) { } } - public static async void WebSocketHandler2(HttpListenerContext ctx) { + public static async void WebSocketHandler(HttpListenerContext ctx) { WebSocketContext wsc; WebSocket ws; try { @@ -62,16 +62,31 @@ public static async void WebSocketHandler2(HttpListenerContext ctx) { await WsWriteText(ws, "{\"connected\":true}"u8.ToArray()); - Task daemon = new Task(async ()=>{ + Task fork = new Task(async ()=>{ while (ws.State == WebSocketState.Open && telnet.Connected) { //host read loop byte[] data = new byte[2048]; - string responseData; + try { int count = stream.Read(data, 0, data.Length); - responseData = Encoding.UTF8.GetString(data, 0, count); - Console.Write(responseData); - await WsWriteText(ws, responseData); + for (int i=0; i(data, 0, count), WebSocketMessageType.Text, true, CancellationToken.None); + + if (count == 0) { // Remote host has closed the connection + if (ws.State == WebSocketState.Open) { + try { + await ws?.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, CancellationToken.None); + } + catch { } + } + return; + } + + //Console.Write(dataString); } catch (System.IO.IOException) { return; @@ -87,7 +102,7 @@ public static async void WebSocketHandler2(HttpListenerContext ctx) { } } }); - daemon.Start(); + fork.Start(); while (ws.State == WebSocketState.Open && telnet.Connected) { //host write loop byte[] buff = new byte[2048]; @@ -130,135 +145,4 @@ public static async void WebSocketHandler2(HttpListenerContext ctx) { catch { } } } - - public static async void WebSocketHandler(HttpListenerContext ctx) { - WebSocketContext wsc; - WebSocket ws; - try { - wsc = await ctx.AcceptWebSocketAsync(null); - ws = wsc.WebSocket; - } - catch (WebSocketException ex) { - ctx.Response.Close(); - Logger.Error(ex); - return; - } - - if (!Auth.IsAuthenticatedAndAuthorized(ctx, ctx.Request.Url.AbsolutePath)) { - await ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); - return; - } - - string sessionId = ctx.Request.Cookies["sessionid"]?.Value ?? null; - -//#if DEBUG - string username = IPAddress.IsLoopback(ctx.Request.RemoteEndPoint.Address) ? "loopback" : Auth.GetUsername(sessionId); -//#else -// string username = Auth.GetUsername(sessionId); -//#endif - - Thread wsToServer = null; - - try { - byte[] targetBuff = new byte[1024]; - WebSocketReceiveResult targetResult = await ws.ReceiveAsync(new ArraySegment(targetBuff), CancellationToken.None); - string target = Encoding.Default.GetString(targetBuff, 0, targetResult.Count); - - string[] split = target.Split(':'); - string host = split[0]; - int port = 23; - - if (split.Length > 1) { - _ = int.TryParse(split[1], out port); - } - - TcpClient telnet; - try { - telnet = new TcpClient(host, port); - } - catch (Exception ex) { - await WsWriteText(ws, ex.Message); - await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, CancellationToken.None); - return; - } - - Logger.Action(username, $"Establish telnet connection to {host}:{port}"); - - //WsWriteText(ws, $"connected to {host}:{port}\n\r"); - - NetworkStream stream = telnet.GetStream(); - - wsToServer = new Thread(async () => { - while (ws.State == WebSocketState.Open) { //ws to server loop - - byte[] buff = new byte[2048]; - WebSocketReceiveResult receiveResult = null!; - try { - receiveResult = await ws.ReceiveAsync(new ArraySegment(buff), CancellationToken.None); - - if (receiveResult.MessageType == WebSocketMessageType.Close) { - await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, CancellationToken.None); - telnet.Close(); - break; - } - } - catch { } - - if (!Auth.IsAuthenticatedAndAuthorized(ctx, "/ws/telnet")) { //check session - ctx.Response.Close(); - telnet.Close(); - return; - } - - try { - for (int i = 0; i < receiveResult?.Count; i++) - stream.Write(buff, i, 1); - stream.Write("\r"u8.ToArray(), 0, 1); //return - } - catch { } - } - }); - - wsToServer.Start(); - - while (ws.State == WebSocketState.Open) { //server to ws loop - byte[] data = new byte[2048]; - - int bytes = stream.Read(data, 0, data.Length); - - string responseData = Encoding.ASCII.GetString(data, 0, bytes); - - if (!Auth.IsAuthenticatedAndAuthorized(ctx, "/ws/telnet")) { //check session - ctx.Response.Close(); - telnet.Close(); - return; - } - - try { - await WsWriteText(ws, responseData); - - } - catch { } - } - - } - catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { - return; - } - catch (WebSocketException ex) when (ex.WebSocketErrorCode != WebSocketError.ConnectionClosedPrematurely) { - Logger.Error(ex); - } - catch (Exception ex) { - Logger.Error(ex); - } - finally { - //wsToServer?.Abort(); - } - if (ws.State == WebSocketState.Open) { - try { - await ws?.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, CancellationToken.None); - } - catch { } - } - } } \ No newline at end of file