From 087322f2b4196642fdecf24792cea69c939a8076 Mon Sep 17 00:00:00 2001 From: Usman Nasir Date: Sat, 2 Nov 2019 19:29:02 +0500 Subject: [PATCH] web terminal --- .idea/workspace.xml | 1350 ++-- CyberCP/settings.py | 3 +- CyberCP/urls.py | 1 + IncBackups/IncBackupsControl.py | 291 +- IncBackups/restoreMeta.py | 6 + IncBackups/static/IncBackups/IncBackups.js | 62 +- .../IncBackups/restoreRemoteBackups.html | 16 +- IncBackups/views.py | 21 +- WebTerminal/CPWebSocket.py | 137 + WebTerminal/__init__.py | 0 WebTerminal/admin.py | 6 + WebTerminal/apps.py | 8 + WebTerminal/cpssh.service | 12 + WebTerminal/migrations/__init__.py | 0 WebTerminal/models.py | 6 + WebTerminal/requirments.txt | 8 + WebTerminal/servCTRL.py | 51 + WebTerminal/static/WebTerminal/main.js | 94 + WebTerminal/static/WebTerminal/term.js | 5977 +++++++++++++++++ WebTerminal/static/WebTerminal/ws.js | 64 + WebTerminal/static/images/loading.gif | Bin 0 -> 33574 bytes .../templates/WebTerminal/WebTerminal.html | 46 + WebTerminal/tests.py | 6 + WebTerminal/urls.py | 6 + WebTerminal/views.py | 42 + .../templates/baseTemplate/index.html | 24 +- install/install.py | 59 + install/litespeed/httpd.conf | 4 +- locale/br/LC_MESSAGES/django.po | 289 +- locale/bs/LC_MESSAGES/django.po | 291 +- locale/cn/LC_MESSAGES/django.po | 291 +- locale/de/LC_MESSAGES/django.po | 289 +- locale/es/LC_MESSAGES/django.po | 289 +- locale/fr/LC_MESSAGES/django.po | 289 +- locale/gr/LC_MESSAGES/django.po | 293 +- locale/id/LC_MESSAGES/django.po | 289 +- locale/it/LC_MESSAGES/django.po | 290 +- locale/ja/LC_MESSAGES/django.mo | Bin 89637 -> 97649 bytes locale/ja/LC_MESSAGES/django.po | 840 ++- locale/pl/LC_MESSAGES/django.po | 289 +- locale/pt/LC_MESSAGES/django.po | 287 +- locale/ru/LC_MESSAGES/django.po | 292 +- locale/tr/LC_MESSAGES/django.po | 287 +- locale/vi/LC_MESSAGES/django.po | 289 +- loginSystem/urls.py | 3 +- mailServer/mailserverManager.py | 4 +- mailServer/models.py | 3 - plogical/mailUtilities.py | 9 + plogical/upgrade.py | 56 + serverStatus/litespeed/httpd.conf | 4 +- .../serverStatus/litespeedStatus.html | 20 + serverStatus/views.py | 21 +- static/IncBackups/IncBackups.js | 177 + static/WebTerminal/main.js | 94 + static/WebTerminal/term.js | 5977 +++++++++++++++++ static/WebTerminal/ws.js | 64 + static/mailServer/mailServer.js | 3 + websiteFunctions/website.py | 1 + 58 files changed, 16489 insertions(+), 3141 deletions(-) create mode 100644 WebTerminal/CPWebSocket.py create mode 100644 WebTerminal/__init__.py create mode 100644 WebTerminal/admin.py create mode 100644 WebTerminal/apps.py create mode 100644 WebTerminal/cpssh.service create mode 100644 WebTerminal/migrations/__init__.py create mode 100644 WebTerminal/models.py create mode 100644 WebTerminal/requirments.txt create mode 100644 WebTerminal/servCTRL.py create mode 100755 WebTerminal/static/WebTerminal/main.js create mode 100755 WebTerminal/static/WebTerminal/term.js create mode 100755 WebTerminal/static/WebTerminal/ws.js create mode 100755 WebTerminal/static/images/loading.gif create mode 100755 WebTerminal/templates/WebTerminal/WebTerminal.html create mode 100644 WebTerminal/tests.py create mode 100755 WebTerminal/urls.py create mode 100644 WebTerminal/views.py mode change 100644 => 100755 mailServer/mailserverManager.py create mode 100644 static/WebTerminal/main.js create mode 100644 static/WebTerminal/term.js create mode 100644 static/WebTerminal/ws.js diff --git a/.idea/workspace.xml b/.idea/workspace.xml index e3ba0cb48..46ea8a993 100755 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -27,97 +27,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -134,36 +48,36 @@ - createDomain - submitDomain - getpass - addRule - firewalld - normal - Removing OpenLiteSpeed.. - def decideServer( - mirror.cyberpanel.net - http - loginAPI - verify - verifylogins - https - kvm - openvz - virtualenv - createWebsiteAPI - selectedEmail - emailForwarding - emailFor - selectForwardingEmail - submitForwardDeletion - getEmailsForDomain - forwardEmail - export restoreRemoteBackupsInc - backupData - backupData( - sftpFunction + status + liteSpeedStatus + saveRewrite + Dashboard + onerror + sendCommand + trans_forward + invoke_shell + self.ssh + webssh + _shell + [ + _check_init_param + Result from paramiko + trans_back + get data + parse_pkey + privaterKey + init + print(self.verifyPath) + CloudLinux + verifyPath + charWidth + sendClientData + cpec + cpecs + emailMig + apt install build-essential libssl-dev libffi-dev python3-dev + python4 admin.api == 1 @@ -194,62 +108,63 @@ @@ -258,7 +173,6 @@ - @@ -269,57 +183,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + +
@@ -95,8 +108,7 @@ - Restore diff --git a/IncBackups/views.py b/IncBackups/views.py index 8276e02ad..b6a42569d 100644 --- a/IncBackups/views.py +++ b/IncBackups/views.py @@ -550,11 +550,21 @@ def restorePoint(request): tempPath = "/home/cyberpanel/" + str(randint(1000, 9999)) - extraArgs = {} - extraArgs['website'] = backupDomain - extraArgs['jobid'] = jobid - extraArgs['tempPath'] = tempPath - extraArgs['reconstruct'] = data['reconstruct'] + if data['reconstruct'] == 'remote': + extraArgs = {} + extraArgs['website'] = backupDomain + extraArgs['jobid'] = jobid + extraArgs['tempPath'] = tempPath + extraArgs['reconstruct'] = data['reconstruct'] + extraArgs['backupDestinations'] = data['backupDestinations'] + extraArgs['password'] = data['password'] + extraArgs['path'] = data['path'] + else: + extraArgs = {} + extraArgs['website'] = backupDomain + extraArgs['jobid'] = jobid + extraArgs['tempPath'] = tempPath + extraArgs['reconstruct'] = data['reconstruct'] startJob = IncJobs('restorePoint', extraArgs) @@ -718,7 +728,6 @@ def restoreRemoteBackups(request): websitesName = ACLManager.findAllSites(currentACL, userID) destinations = [] - destinations.append('local') path = '/home/cyberpanel/sftp' diff --git a/WebTerminal/CPWebSocket.py b/WebTerminal/CPWebSocket.py new file mode 100644 index 000000000..7dd379fe1 --- /dev/null +++ b/WebTerminal/CPWebSocket.py @@ -0,0 +1,137 @@ +import os +import asyncio +import websockets +import paramiko +import json +import ssl + + +class WebSocketServer(): + + def loadPublicKey(self): + pubkey = '/root/.ssh/cyberpanel.pub' + + data = open(pubkey, 'r').read() + + authFile = '/root/.ssh/authorized_keys' + + authData = open(authFile, 'r').read() + + checker = 1 + + if authData.find(data) > -1: + checker = 0 + + if checker: + writeToFile = open(authFile, 'a') + writeToFile.writelines(data) + writeToFile.close() + + + def __init__(self, websocket, path): + self.websockets = websocket + self.path = path + self.sshclient = paramiko.SSHClient() + self.sshclient.load_system_host_keys() + self.sshclient.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + k = paramiko.RSAKey.from_private_key_file('/root/.ssh/cyberpanel') + + ## Load Public Key + self.loadPublicKey() + + self.sshclient.connect('127.0.0.1', 22, username='root', pkey=k) + self.shell = self.sshclient.invoke_shell(term='xterm') + self.shell.settimeout(0) + self.verifyPath = '' + + async def consumer_handler(self): + try: + async for message in self.websockets: + await self.sendData(message) + except: + print(self.verifyPath) + os.remove(self.verifyPath) + + async def producer_handler(self): + try: + while True: + message = await self.recvData() + if os.path.exists(self.verifyPath): + await self.websockets.send(message) + else: + await self.websockets.send('Authentication failed.') + except: + print(self.verifyPath) + os.remove(self.verifyPath) + + async def recvData(self): + try: + print ('recvData') + try: + while True: + if self.shell.recv_ready(): + return self.shell.recv(9000).decode("utf-8") + else: + await asyncio.sleep(0.1) + continue + except: + pass + except: + print(self.verifyPath) + os.remove(self.verifyPath) + + async def sendData(self, message): + try: + print ('sendData') + print (str(message)) + try: + data = json.loads(message) + if str(message).find('"tp":"init"') > -1: + self.verifyPath = str(data['data']['verifyPath']) + else: + if os.path.exists(self.verifyPath): + self.shell.send(str(data['data'])) + except: + pass + except: + print(self.verifyPath) + os.remove(self.verifyPath) + + @staticmethod + async def initialize(websocket, path): + try: + webshell = WebSocketServer(websocket, path) + + consumer_task = asyncio.ensure_future( + webshell.consumer_handler()) + producer_task = asyncio.ensure_future( + webshell.producer_handler()) + done, pending = await asyncio.wait( + [consumer_task, producer_task], + return_when=asyncio.FIRST_COMPLETED, + ) + for task in pending: + task.cancel() + except: + print(webshell.verifyPath) + os.remove(webshell.verifyPath) + + +def main(): + pidfile = '/usr/local/CyberCP/WebTerminal/pid' + machineIP = open('/etc/cyberpanel/machineIP', 'r').read() + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain('/usr/local/lscp/conf/cert.pem', '/usr/local/lscp/conf/key.pem') + start_server = websockets.serve(WebSocketServer.initialize, machineIP, 5678, ssl=context) + asyncio.get_event_loop().run_until_complete(start_server) + asyncio.get_event_loop().run_forever() + + writeToFile = open(pidfile, 'r') + writeToFile.write(str(os.getpid())) + writeToFile.close() + + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/WebTerminal/__init__.py b/WebTerminal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/WebTerminal/admin.py b/WebTerminal/admin.py new file mode 100644 index 000000000..13be29d96 --- /dev/null +++ b/WebTerminal/admin.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin + +# Register your models here. diff --git a/WebTerminal/apps.py b/WebTerminal/apps.py new file mode 100644 index 000000000..b53c3b4c6 --- /dev/null +++ b/WebTerminal/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class WebterminalConfig(AppConfig): + name = 'WebTerminal' diff --git a/WebTerminal/cpssh.service b/WebTerminal/cpssh.service new file mode 100644 index 000000000..5f99bca49 --- /dev/null +++ b/WebTerminal/cpssh.service @@ -0,0 +1,12 @@ +[Unit] +Description = CyberPanel SSH Websocket Daemon + +[Service] +Type=forking +ExecStart = /usr/local/CyberPanel/p3/bin/python3 /usr/local/CyberCP/WebTerminal/servCTRL.py start +ExecStop = /usr/local/CyberPanel/p3/bin/python3 /usr/local/CyberCP/WebTerminal/servCTRL.py stop +Restart = /usr/local/CyberPanel/p3/bin/python3 /usr/local/CyberCP/WebTerminal/servCTRL.py restart +Restart=on-abnormal + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/WebTerminal/migrations/__init__.py b/WebTerminal/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/WebTerminal/models.py b/WebTerminal/models.py new file mode 100644 index 000000000..1dfab7604 --- /dev/null +++ b/WebTerminal/models.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. diff --git a/WebTerminal/requirments.txt b/WebTerminal/requirments.txt new file mode 100644 index 000000000..d4ec218e5 --- /dev/null +++ b/WebTerminal/requirments.txt @@ -0,0 +1,8 @@ +bcrypt==3.1.7 +cffi==1.13.1 +cryptography==2.8 +paramiko==2.6.0 +pycparser==2.19 +PyNaCl==1.3.0 +six==1.12.0 +websockets==8.0.2 \ No newline at end of file diff --git a/WebTerminal/servCTRL.py b/WebTerminal/servCTRL.py new file mode 100644 index 000000000..5c65be9ec --- /dev/null +++ b/WebTerminal/servCTRL.py @@ -0,0 +1,51 @@ +import subprocess +import shlex +import argparse +import os + + + +class servCTRL: + pidfile = '/usr/local/CyberCP/WebTerminal/pid' + + def prepareArguments(self): + + parser = argparse.ArgumentParser(description='CyberPanel Policy Control Parser!') + parser.add_argument('function', help='Specific a operation to perform!') + + return parser.parse_args() + + def start(self): + + if os.path.exists(servCTRL.pidfile): + self.stop() + + command = '/usr/local/CyberPanel/p3/bin/python3 /usr/local/CyberCP/WebTerminal/CPWebSocket.py' + subprocess.Popen(shlex.split(command)) + + def stop(self): + try: + path = servCTRL.pidfile + command = 'kill -9 %s' % (open(path, 'r').read()) + subprocess.Popen(shlex.split(command)) + except: + pass + + +def main(): + + policy = servCTRL() + args = policy.prepareArguments() + + ## Website functions + + if args.function == "start": + policy.start() + elif args.function == "stop": + policy.stop() + elif args.function == "restart": + policy.stop() + policy.start() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/WebTerminal/static/WebTerminal/main.js b/WebTerminal/static/WebTerminal/main.js new file mode 100755 index 000000000..6b6ea8a2b --- /dev/null +++ b/WebTerminal/static/WebTerminal/main.js @@ -0,0 +1,94 @@ +var charWidth = 6.2; +var charHeight = 15.2; + +/** + * for full screen + * @returns {{w: number, h: number}} + */ +function getTerminalSize() { + var width = window.innerWidth; + var height = window.innerHeight; + return { + w: Math.floor(width / charWidth), + h: Math.floor(height / charHeight) + }; +} + + + +function openTerminal(options) { + if (!$.isEmptyObject($('.terminal')[0])) { + alert("Please refresh this page."); + return + } + + var client = new WSSHClient(); + var term = new Terminal({cols: 120, rows: 30, screenKeys: true, useStyle: true}); + term.on('data', function (data) { + client.sendClientData(data); + }); + term.open(); + $('.terminal').detach().appendTo('#term'); + $("#term").show(); + term.write('Connecting...' + '\r\n'); + + client.connect({ + onError: function (error) { + term.write('Error connecting to backend.\r\n'); + //term.destroy(); + }, + onConnect: function () { + client.sendInitData(options); + term.write('connection established..\r\n'); + }, + onClose: function (e) { + term.write("\r\nconnection closed.") + //term.destroy(); + }, + onData: function (data) { + term.write(data); + } + }) + +} + +function store(options) { + window.localStorage.host = options.host; + window.localStorage.port = options.port; + window.localStorage.username = options.username; + window.localStorage.ispwd = options.ispwd; + window.localStorage.secret = options.secret +} + +function check() { + return validResult["host"] && validResult["port"] && validResult["username"]; +} + +function connect() { + var remember = $("#remember").is(":checked"); + var options = { + host: $("#host").val(), + port: $("#port").val(), + username: $("#username").val(), + ispwd: $("input[name=ispwd]:checked").val(), + secret: $("#secret").val(), + verifyPath: $("#verifyPath").text() + } + console.debug(options); + if (remember) { + store(options) + } + // if (check()) { + // openTerminal(options) + // } else { + // for (var key in validResult) { + // if (!validResult[key]) { + // alert(errorMsg[key]); + // break; + // } + // } + // } + openTerminal(options) +} + +connect(); \ No newline at end of file diff --git a/WebTerminal/static/WebTerminal/term.js b/WebTerminal/static/WebTerminal/term.js new file mode 100755 index 000000000..68bc07343 --- /dev/null +++ b/WebTerminal/static/WebTerminal/term.js @@ -0,0 +1,5977 @@ +/** + * term.js - an xterm emulator + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */ + +;(function() { + +/** + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */ + +'use strict'; + +/** + * Shared + */ + +var window = this + , document = this.document; + +/** + * EventEmitter + */ + +function EventEmitter() { + this._events = this._events || {}; +} + +EventEmitter.prototype.addListener = function(type, listener) { + this._events[type] = this._events[type] || []; + this._events[type].push(listener); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.removeListener = function(type, listener) { + if (!this._events[type]) return; + + var obj = this._events[type] + , i = obj.length; + + while (i--) { + if (obj[i] === listener || obj[i].listener === listener) { + obj.splice(i, 1); + return; + } + } +}; + +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + +EventEmitter.prototype.removeAllListeners = function(type) { + if (this._events[type]) delete this._events[type]; +}; + +EventEmitter.prototype.once = function(type, listener) { + function on() { + var args = Array.prototype.slice.call(arguments); + this.removeListener(type, on); + return listener.apply(this, args); + } + on.listener = listener; + return this.on(type, on); +}; + +EventEmitter.prototype.emit = function(type) { + if (!this._events[type]) return; + + var args = Array.prototype.slice.call(arguments, 1) + , obj = this._events[type] + , l = obj.length + , i = 0; + + for (; i < l; i++) { + obj[i].apply(this, args); + } +}; + +EventEmitter.prototype.listeners = function(type) { + return this._events[type] = this._events[type] || []; +}; + +/** + * Stream + */ + +function Stream() { + EventEmitter.call(this); +} + +inherits(Stream, EventEmitter); + +Stream.prototype.pipe = function(dest, options) { + var src = this + , ondata + , onerror + , onend; + + function unbind() { + src.removeListener('data', ondata); + src.removeListener('error', onerror); + src.removeListener('end', onend); + dest.removeListener('error', onerror); + dest.removeListener('close', unbind); + } + + src.on('data', ondata = function(data) { + dest.write(data); + }); + + src.on('error', onerror = function(err) { + unbind(); + if (!this.listeners('error').length) { + throw err; + } + }); + + src.on('end', onend = function() { + dest.end(); + unbind(); + }); + + dest.on('error', onerror); + dest.on('close', unbind); + + dest.emit('pipe', src); + + return dest; +}; + +/** + * States + */ + +var normal = 0 + , escaped = 1 + , csi = 2 + , osc = 3 + , charset = 4 + , dcs = 5 + , ignore = 6 + , UDK = { type: 'udk' }; + +/** + * Terminal + */ + +function Terminal(options) { + var self = this; + + if (!(this instanceof Terminal)) { + return new Terminal(arguments[0], arguments[1], arguments[2]); + } + + Stream.call(this); + + if (typeof options === 'number') { + options = { + cols: arguments[0], + rows: arguments[1], + handler: arguments[2] + }; + } + + options = options || {}; + + each(keys(Terminal.defaults), function(key) { + if (options[key] == null) { + options[key] = Terminal.options[key]; + // Legacy: + if (Terminal[key] !== Terminal.defaults[key]) { + options[key] = Terminal[key]; + } + } + self[key] = options[key]; + }); + + if (options.colors.length === 8) { + options.colors = options.colors.concat(Terminal._colors.slice(8)); + } else if (options.colors.length === 16) { + options.colors = options.colors.concat(Terminal._colors.slice(16)); + } else if (options.colors.length === 10) { + options.colors = options.colors.slice(0, -2).concat( + Terminal._colors.slice(8, -2), options.colors.slice(-2)); + } else if (options.colors.length === 18) { + options.colors = options.colors.slice(0, -2).concat( + Terminal._colors.slice(16, -2), options.colors.slice(-2)); + } + this.colors = options.colors; + + this.options = options; + + // this.context = options.context || window; + // this.document = options.document || document; + this.parent = options.body || options.parent + || (document ? document.getElementsByTagName('body')[0] : null); + + this.cols = options.cols || options.geometry[0]; + this.rows = options.rows || options.geometry[1]; + + // Act as though we are a node TTY stream: + this.setRawMode; + this.isTTY = true; + this.isRaw = true; + this.columns = this.cols; + this.rows = this.rows; + + if (options.handler) { + this.on('data', options.handler); + } + + this.ybase = 0; + this.ydisp = 0; + this.x = 0; + this.y = 0; + this.cursorState = 0; + this.cursorHidden = false; + this.convertEol; + this.state = 0; + this.queue = ''; + this.scrollTop = 0; + this.scrollBottom = this.rows - 1; + + // modes + this.applicationKeypad = false; + this.applicationCursor = false; + this.originMode = false; + this.insertMode = false; + this.wraparoundMode = false; + this.normal = null; + + // select modes + this.prefixMode = false; + this.selectMode = false; + this.visualMode = false; + this.searchMode = false; + this.searchDown; + this.entry = ''; + this.entryPrefix = 'Search: '; + this._real; + this._selected; + this._textarea; + + // charset + this.charset = null; + this.gcharset = null; + this.glevel = 0; + this.charsets = [null]; + + // mouse properties + this.decLocator; + this.x10Mouse; + this.vt200Mouse; + this.vt300Mouse; + this.normalMouse; + this.mouseEvents; + this.sendFocus; + this.utfMouse; + this.sgrMouse; + this.urxvtMouse; + + // misc + this.element; + this.children; + this.refreshStart; + this.refreshEnd; + this.savedX; + this.savedY; + this.savedCols; + + // stream + this.readable = true; + this.writable = true; + + this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); + this.curAttr = this.defAttr; + + this.params = []; + this.currentParam = 0; + this.prefix = ''; + this.postfix = ''; + + this.lines = []; + var i = this.rows; + while (i--) { + this.lines.push(this.blankLine()); + } + + this.tabs; + this.setupStops(); +} + +inherits(Terminal, Stream); + +/** + * Colors + */ + +// Colors 0-15 +Terminal.tangoColors = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' +]; + +Terminal.xtermColors = [ + // dark: + '#000000', // black + '#cd0000', // red3 + '#00cd00', // green3 + '#cdcd00', // yellow3 + '#0000ee', // blue2 + '#cd00cd', // magenta3 + '#00cdcd', // cyan3 + '#e5e5e5', // gray90 + // bright: + '#7f7f7f', // gray50 + '#ff0000', // red + '#00ff00', // green + '#ffff00', // yellow + '#5c5cff', // rgb:5c/5c/ff + '#ff00ff', // magenta + '#00ffff', // cyan + '#ffffff' // white +]; + +// Colors 0-15 + 16-255 +// Much thanks to TooTallNate for writing this. +Terminal.colors = (function() { + var colors = Terminal.tangoColors.slice() + , r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] + , i; + + // 16-231 + i = 0; + for (; i < 216; i++) { + out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); + } + + // 232-255 (grey) + i = 0; + for (; i < 24; i++) { + r = 8 + i * 10; + out(r, r, r); + } + + function out(r, g, b) { + colors.push('#' + hex(r) + hex(g) + hex(b)); + } + + function hex(c) { + c = c.toString(16); + return c.length < 2 ? '0' + c : c; + } + + return colors; +})(); + +// Default BG/FG +Terminal.colors[256] = '#000000'; +Terminal.colors[257] = '#f0f0f0'; + +Terminal._colors = Terminal.colors.slice(); + +Terminal.vcolors = (function() { + var out = [] + , colors = Terminal.colors + , i = 0 + , color; + + for (; i < 256; i++) { + color = parseInt(colors[i].substring(1), 16); + out.push([ + (color >> 16) & 0xff, + (color >> 8) & 0xff, + color & 0xff + ]); + } + + return out; +})(); + +/** + * Options + */ + +Terminal.defaults = { + colors: Terminal.colors, + convertEol: false, + termName: 'xterm', + geometry: [80, 24], + cursorBlink: true, + visualBell: false, + popOnBell: false, + scrollback: 1000, + screenKeys: false, + debug: false, + useStyle: false + // programFeatures: false, + // focusKeys: false, +}; + +Terminal.options = {}; + +each(keys(Terminal.defaults), function(key) { + Terminal[key] = Terminal.defaults[key]; + Terminal.options[key] = Terminal.defaults[key]; +}); + +/** + * Focused Terminal + */ + +Terminal.focus = null; + +Terminal.prototype.focus = function() { + if (Terminal.focus === this) return; + + if (Terminal.focus) { + Terminal.focus.blur(); + } + + if (this.sendFocus) this.send('\x1b[I'); + this.showCursor(); + + // try { + // this.element.focus(); + // } catch (e) { + // ; + // } + + // this.emit('focus'); + + Terminal.focus = this; +}; + +Terminal.prototype.blur = function() { + if (Terminal.focus !== this) return; + + this.cursorState = 0; + this.refresh(this.y, this.y); + if (this.sendFocus) this.send('\x1b[O'); + + // try { + // this.element.blur(); + // } catch (e) { + // ; + // } + + // this.emit('blur'); + + Terminal.focus = null; +}; + +/** + * Initialize global behavior + */ + +Terminal.prototype.initGlobal = function() { + var document = this.document; + + Terminal._boundDocs = Terminal._boundDocs || []; + if (~indexOf(Terminal._boundDocs, document)) { + return; + } + Terminal._boundDocs.push(document); + + Terminal.bindPaste(document); + + Terminal.bindKeys(document); + + Terminal.bindCopy(document); + + if (this.isMobile) { + this.fixMobile(document); + } + + if (this.useStyle) { + Terminal.insertStyle(document, this.colors[256], this.colors[257]); + } +}; + +/** + * Bind to paste event + */ + +Terminal.bindPaste = function(document) { + // This seems to work well for ctrl-V and middle-click, + // even without the contentEditable workaround. + var window = document.defaultView; + on(window, 'paste', function(ev) { + var term = Terminal.focus; + if (!term) return; + if (ev.clipboardData) { + term.send(ev.clipboardData.getData('text/plain')); + } else if (term.context.clipboardData) { + term.send(term.context.clipboardData.getData('Text')); + } + // Not necessary. Do it anyway for good measure. + term.element.contentEditable = 'inherit'; + return cancel(ev); + }); +}; + +/** + * Global Events for key handling + */ + +Terminal.bindKeys = function(document) { + // We should only need to check `target === body` below, + // but we can check everything for good measure. + on(document, 'keydown', function(ev) { + if (!Terminal.focus) return; + var target = ev.target || ev.srcElement; + if (!target) return; + if (target === Terminal.focus.element + || target === Terminal.focus.context + || target === Terminal.focus.document + || target === Terminal.focus.body + || target === Terminal._textarea + || target === Terminal.focus.parent) { + return Terminal.focus.keyDown(ev); + } + }, true); + + on(document, 'keypress', function(ev) { + if (!Terminal.focus) return; + var target = ev.target || ev.srcElement; + if (!target) return; + if (target === Terminal.focus.element + || target === Terminal.focus.context + || target === Terminal.focus.document + || target === Terminal.focus.body + || target === Terminal._textarea + || target === Terminal.focus.parent) { + return Terminal.focus.keyPress(ev); + } + }, true); + + // If we click somewhere other than a + // terminal, unfocus the terminal. + on(document, 'mousedown', function(ev) { + if (!Terminal.focus) return; + + var el = ev.target || ev.srcElement; + if (!el) return; + + do { + if (el === Terminal.focus.element) return; + } while (el = el.parentNode); + + Terminal.focus.blur(); + }); +}; + +/** + * Copy Selection w/ Ctrl-C (Select Mode) + */ + +Terminal.bindCopy = function(document) { + var window = document.defaultView; + + // if (!('onbeforecopy' in document)) { + // // Copies to *only* the clipboard. + // on(window, 'copy', function fn(ev) { + // var term = Terminal.focus; + // if (!term) return; + // if (!term._selected) return; + // var text = term.grabText( + // term._selected.x1, term._selected.x2, + // term._selected.y1, term._selected.y2); + // term.emit('copy', text); + // ev.clipboardData.setData('text/plain', text); + // }); + // return; + // } + + // Copies to primary selection *and* clipboard. + // NOTE: This may work better on capture phase, + // or using the `beforecopy` event. + on(window, 'copy', function(ev) { + var term = Terminal.focus; + if (!term) return; + if (!term._selected) return; + var textarea = term.getCopyTextarea(); + var text = term.grabText( + term._selected.x1, term._selected.x2, + term._selected.y1, term._selected.y2); + term.emit('copy', text); + textarea.focus(); + textarea.textContent = text; + textarea.value = text; + textarea.setSelectionRange(0, text.length); + setTimeout(function() { + term.element.focus(); + term.focus(); + }, 1); + }); +}; + +/** + * Fix Mobile + */ + +Terminal.prototype.fixMobile = function(document) { + var self = this; + + var textarea = document.createElement('textarea'); + textarea.style.position = 'absolute'; + textarea.style.left = '-32000px'; + textarea.style.top = '-32000px'; + textarea.style.width = '0px'; + textarea.style.height = '0px'; + textarea.style.opacity = '0'; + textarea.style.backgroundColor = 'transparent'; + textarea.style.borderStyle = 'none'; + textarea.style.outlineStyle = 'none'; + textarea.autocapitalize = 'none'; + textarea.autocorrect = 'off'; + + document.getElementsByTagName('body')[0].appendChild(textarea); + + Terminal._textarea = textarea; + + setTimeout(function() { + textarea.focus(); + }, 1000); + + if (this.isAndroid) { + on(textarea, 'change', function() { + var value = textarea.textContent || textarea.value; + textarea.value = ''; + textarea.textContent = ''; + self.send(value + '\r'); + }); + } +}; + +/** + * Insert a default style + */ + +Terminal.insertStyle = function(document, bg, fg) { + var style = document.getElementById('term-style'); + if (style) return; + + var head = document.getElementsByTagName('head')[0]; + if (!head) return; + + var style = document.createElement('style'); + style.id = 'term-style'; + + // textContent doesn't work well with IE for