shellinabox/shellinabox/shell_in_a_box.jspp

481 lines
17 KiB
JavaScript

// ShellInABox.js -- Use XMLHttpRequest to provide an AJAX terminal emulator.
// Copyright (C) 2008-2010 Markus Gutschke <markus@shellinabox.com>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// In addition to these license terms, the author grants the following
// additional rights:
//
// If you modify this program, or any covered work, by linking or
// combining it with the OpenSSL project's OpenSSL library (or a
// modified version of that library), containing parts covered by the
// terms of the OpenSSL or SSLeay licenses, the author
// grants you additional permission to convey the resulting work.
// Corresponding Source for a non-source form of such a combination
// shall include the source code for the parts of OpenSSL used as well
// as that of the covered work.
//
// You may at your option choose to remove this additional permission from
// the work, or from any part of it.
//
// It is possible to build this program in a way that it loads OpenSSL
// libraries at run-time. If doing so, the following notices are required
// by the OpenSSL and SSLeay licenses:
//
// This product includes software developed by the OpenSSL Project
// for use in the OpenSSL Toolkit. (http://www.openssl.org/)
//
// This product includes cryptographic software written by Eric Young
// (eay@cryptsoft.com)
//
//
// The most up-to-date version of this program is always available from
// http://shellinabox.com
//
//
// Notes:
//
// The author believes that for the purposes of this license, you meet the
// requirements for publishing the source code, if your web server publishes
// the source in unmodified form (i.e. with licensing information, comments,
// formatting, and identifier names intact). If there are technical reasons
// that require you to make changes to the source code when serving the
// JavaScript (e.g to remove pre-processor directives from the source), these
// changes should be done in a reversible fashion.
//
// The author does not consider websites that reference this script in
// unmodified form, and web servers that serve this script in unmodified form
// to be derived works. As such, they are believed to be outside of the
// scope of this license and not subject to the rights or restrictions of the
// GNU General Public License.
//
// If in doubt, consult a legal professional familiar with the laws that
// apply in your country.
#define XHR_UNITIALIZED 0
#define XHR_OPEN 1
#define XHR_SENT 2
#define XHR_RECEIVING 3
#define XHR_LOADED 4
// IE does not define XMLHttpRequest by default, so we provide a suitable
// wrapper.
if (typeof XMLHttpRequest == 'undefined') {
XMLHttpRequest = function() {
try { return new ActiveXObject('Msxml2.XMLHTTP.6.0');} catch (e) { }
try { return new ActiveXObject('Msxml2.XMLHTTP.3.0');} catch (e) { }
try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { }
try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) { }
throw new Error('');
};
}
function extend(subClass, baseClass) {
function inheritance() { }
inheritance.prototype = baseClass.prototype;
subClass.prototype = new inheritance();
subClass.prototype.constructor = subClass;
subClass.prototype.superClass = baseClass.prototype;
};
function ShellInABox(url, container) {
if (url == undefined) {
this.rooturl = document.location.href;
this.url = document.location.href.replace(/[?#].*/, '');
} else {
this.rooturl = url;
this.url = url;
}
if (document.location.hash != '') {
var hash = decodeURIComponent(document.location.hash).
replace(/^#/, '');
this.nextUrl = hash.replace(/,.*/, '');
this.session = hash.replace(/[^,]*,/, '');
} else {
this.nextUrl = this.url;
this.session = null;
}
this.pendingKeys = '';
this.keysInFlight = false;
this.connected = false;
this.replayOnOutput = false;
this.replayOnSession = false;
this.superClass.constructor.call(this, container);
// We have to initiate the first XMLHttpRequest from a timer. Otherwise,
// Chrome never realizes that the page has loaded.
setTimeout(function(shellInABox) {
return function() {
shellInABox.messageInit();
shellInABox.sendRequest();
};
}(this), 1);
};
extend(ShellInABox, VT100);
ShellInABox.prototype.sessionClosed = function() {
try {
this.connected = false;
if (this.session) {
this.session = undefined;
if (this.cursorX > 0) {
this.vt100('\r\n');
}
this.vt100('Session closed.');
}
this.showReconnect(true);
if (this.replayOnSession) {
this.messageReplay('session', 'closed');
}
} catch (e) {
}
};
ShellInABox.prototype.reconnect = function() {
this.showReconnect(false);
if (!this.session) {
if (document.location.hash != '') {
// A shellinaboxd daemon launched from a CGI only allows a single
// session. In order to reconnect, we must reload the frame definition
// and obtain a new port number. As this is a different origin, we
// need to get enclosing page to help us.
parent.location = this.nextUrl;
} else {
if (this.url != this.nextUrl) {
document.location.replace(this.nextUrl);
} else {
this.pendingKeys = '';
this.keysInFlight = false;
this.reset(true);
this.sendRequest();
}
}
}
return false;
};
ShellInABox.prototype.sendRequest = function(request) {
if (request == undefined) {
request = new XMLHttpRequest();
}
request.open('POST', this.url + '?', true);
request.timeout = 30000; // Don't leave POST pending forever: force 30s timeout to prevent HTTP Proxy thread hijack
request.setRequestHeader('Cache-Control', 'no-cache');
request.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded; charset=utf-8');
var content = 'width=' + this.terminalWidth +
'&height=' + this.terminalHeight +
(this.session ? '&session=' +
encodeURIComponent(this.session) : '&rooturl='+
encodeURIComponent(this.rooturl));
request.onreadystatechange = function(shellInABox) {
return function() {
try {
return shellInABox.onReadyStateChange(request);
} catch (e) {
shellInABox.sessionClosed();
}
}
}(this);
request.send(content);
};
ShellInABox.prototype.onReadyStateChange = function(request) {
if (request.readyState == XHR_LOADED) {
if (request.status == 200) {
this.connected = true;
var response = eval('(' + request.responseText + ')');
if (response.data) {
if (this.replayOnOutput) {
this.messageReplay('output', response.data);
}
this.vt100(response.data);
}
if (!response.session ||
this.session && this.session != response.session) {
this.sessionClosed();
} else {
if (this.replayOnSession && !this.session && response.session) {
this.messageReplay('session', 'alive');
}
this.session = response.session;
this.sendRequest(request);
}
} else if (request.status == 0) {
// Time Out or other connection problems: retry after 1s to prevent release CPU before retry
setTimeout(function(shellInABox) {
return function() {
shellInABox.sendRequest();
};
}(this), 1000);
} else {
this.sessionClosed();
}
}
};
ShellInABox.prototype.sendKeys = function(keys) {
if (!this.connected) {
return;
}
if (this.keysInFlight || this.session == undefined) {
this.pendingKeys += keys;
} else {
this.keysInFlight = true;
keys = this.pendingKeys + keys;
this.pendingKeys = '';
var request = new XMLHttpRequest();
request.open('POST', this.url + '?', true);
request.setRequestHeader('Cache-Control', 'no-cache');
request.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded; charset=utf-8');
var content = 'width=' + this.terminalWidth +
'&height=' + this.terminalHeight +
'&session=' +encodeURIComponent(this.session)+
'&keys=' + encodeURIComponent(keys);
request.onreadystatechange = function(shellInABox) {
return function() {
try {
return shellInABox.keyPressReadyStateChange(request);
} catch (e) {
}
}
}(this);
request.send(content);
}
};
ShellInABox.prototype.keyPressReadyStateChange = function(request) {
if (request.readyState == XHR_LOADED) {
this.keysInFlight = false;
if (this.pendingKeys) {
this.sendKeys('');
}
}
};
ShellInABox.prototype.keysPressed = function(ch) {
var hex = '0123456789ABCDEF';
var s = '';
for (var i = 0; i < ch.length; i++) {
var c = ch.charCodeAt(i);
if (c < 128) {
s += hex.charAt(c >> 4) + hex.charAt(c & 0xF);
} else if (c < 0x800) {
s += hex.charAt(0xC + (c >> 10) ) +
hex.charAt( (c >> 6) & 0xF ) +
hex.charAt(0x8 + ((c >> 4) & 0x3)) +
hex.charAt( c & 0xF );
} else if (c < 0x10000) {
s += 'E' +
hex.charAt( (c >> 12) ) +
hex.charAt(0x8 + ((c >> 10) & 0x3)) +
hex.charAt( (c >> 6) & 0xF ) +
hex.charAt(0x8 + ((c >> 4) & 0x3)) +
hex.charAt( c & 0xF );
} else if (c < 0x110000) {
s += 'F' +
hex.charAt( (c >> 18) ) +
hex.charAt(0x8 + ((c >> 16) & 0x3)) +
hex.charAt( (c >> 12) & 0xF ) +
hex.charAt(0x8 + ((c >> 10) & 0x3)) +
hex.charAt( (c >> 6) & 0xF ) +
hex.charAt(0x8 + ((c >> 4) & 0x3)) +
hex.charAt( c & 0xF );
}
}
this.sendKeys(s);
};
ShellInABox.prototype.resized = function(w, h) {
// Do not send a resize request until we are fully initialized.
if (this.session) {
// sendKeys() always transmits the current terminal size. So, flush all
// pending keys.
this.sendKeys('');
}
};
ShellInABox.prototype.toggleSSL = function() {
if (document.location.hash != '') {
if (this.nextUrl.match(/\?plain$/)) {
this.nextUrl = this.nextUrl.replace(/\?plain$/, '');
} else {
this.nextUrl = this.nextUrl.replace(/[?#].*/, '') + '?plain';
}
if (!this.session) {
parent.location = this.nextUrl;
}
} else {
this.nextUrl = this.nextUrl.match(/^https:/)
? this.nextUrl.replace(/^https:/, 'http:').replace(/\/*$/, '/plain')
: this.nextUrl.replace(/^http/, 'https').replace(/\/*plain$/, '');
}
if (this.nextUrl.match(/^[:]*:\/\/[^/]*$/)) {
this.nextUrl += '/';
}
if (this.session && this.nextUrl != this.url) {
alert('This change will take effect the next time you login.');
}
};
ShellInABox.prototype.extendContextMenu = function(entries, actions) {
// Modify the entries and actions in place, adding any locally defined
// menu entries.
var oldActions = [ ];
for (var i = 0; i < actions.length; i++) {
oldActions[i] = actions[i];
}
for (var node = entries.firstChild, i = 0, j = 0; node;
node = node.nextSibling) {
if (node.tagName == 'LI') {
actions[i++] = oldActions[j++];
if (node.id == "endconfig") {
node.id = '';
if (typeof serverSupportsSSL != 'undefined' && serverSupportsSSL &&
!(typeof disableSSLMenu != 'undefined' && disableSSLMenu)) {
// If the server supports both SSL and plain text connections,
// provide a menu entry to switch between the two.
var newNode = document.createElement('li');
var isSecure;
if (document.location.hash != '') {
isSecure = !this.nextUrl.match(/\?plain$/);
} else {
isSecure = this.nextUrl.match(/^https:/);
}
newNode.innerHTML = (isSecure ? '&#10004; ' : '') + 'Secure';
if (node.nextSibling) {
entries.insertBefore(newNode, node.nextSibling);
} else {
entries.appendChild(newNode);
}
actions[i++] = this.toggleSSL;
node = newNode;
}
node.id = 'endconfig';
}
}
}
};
ShellInABox.prototype.messageInit = function() {
// Test if server option for iframe message passing was set.
if (!serverMessagesOrigin) {
return;
}
// Test for browser support of this feature. JSON class functionality is
// also needed because some older IE browsers, support only string passing
// and we don't want to use unsafe eval() function in this case.
if (!window.postMessage || !window.JSON ||
!window.JSON.parse || !window.JSON.stringify) {
return;
}
// Install event listener.
if (window.addEventListener) {
window.addEventListener('message', function(shellInABox) {
return function(message) {
shellInABox.messageReceive(message);
}
}(this), false);
} else {
// For IE8
if (window.attachEvent) {
window.attachEvent('onmessage', function(shellInABox) {
return function(message) {
shellInABox.messageReceive(message);
}
}(this));
}
}
};
ShellInABox.prototype.messageReceive = function (message) {
// Check for message origin if needed.
if (serverMessagesOrigin !== "*") {
if (serverMessagesOrigin !== message.origin) {
return;
}
}
// Remember replay information.
if (!this.replaySource || !this.replayOrigin) {
this.replaySource = message.source;
this.replayOrigin = message.origin;
}
// Handle received message.
var decoded = JSON.parse(message.data);
switch (decoded.type) {
case 'input' :
// Input received data to terminal.
this.keysPressed(decoded.data);
break;
case 'output' :
// Enable, disable or toggle passing terminal output to parent window.
switch (decoded.data) {
case 'enable' : this.replayOnOutput = true; break;
case 'disable' : this.replayOnOutput = false; break;
case 'toggle' : this.replayOnOutput = !this.replayOnOutput; break;
}
break;
case 'session':
// Replay with session status.
this.messageReplay('session', this.session ? 'alive' : 'closed');
break;
case 'onsessionchange':
// Enable, disable or toggle passing session status to parent window.
switch (decoded.data) {
case 'enable' : this.replayOnSession = true; break;
case 'disable' : this.replayOnSession = false; break;
case 'toggle' : this.replayOnSession = !this.replayOnSession; break;
}
break;
case 'reconnect':
this.reconnect();
break;
}
};
ShellInABox.prototype.messageReplay = function(type, data) {
if (this.replaySource && this.replayOrigin) {
var encoded = JSON.stringify({ type : type, data : data });
this.replaySource.postMessage(encoded, this.replayOrigin);
}
};
ShellInABox.prototype.about = function() {
alert("Shell In A Box " + VERSION +
"\n\n" +
"Copyright 2008-2015 by Markus Gutschke. For more information visit\n" +
"http://shellinabox.com or http://github.com/shellinabox/." +
(typeof serverSupportsSSL != 'undefined' && serverSupportsSSL ?
"\n\n" +
"This product includes software developed by the OpenSSL Project for\n" +
"use in the OpenSSL Toolkit. (http://www.openssl.org/)" +
"\n\n" +
"This product includes cryptographic software written by Eric Young\n" +
"(eay@cryptsoft.com)" :
""));
};
/* vim: set filetype=javascript : */