1 // ShellInABox.js -- Use XMLHttpRequest to provide an AJAX terminal emulator.
2 // Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com>
4 // This program is free software; you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License version 2 as
6 // published by the Free Software Foundation.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License along
14 // with this program; if not, write to the Free Software Foundation, Inc.,
15 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 // In addition to these license terms, the author grants the following
20 // If you modify this program, or any covered work, by linking or
21 // combining it with the OpenSSL project's OpenSSL library (or a
22 // modified version of that library), containing parts covered by the
23 // terms of the OpenSSL or SSLeay licenses, the author
24 // grants you additional permission to convey the resulting work.
25 // Corresponding Source for a non-source form of such a combination
26 // shall include the source code for the parts of OpenSSL used as well
27 // as that of the covered work.
29 // You may at your option choose to remove this additional permission from
30 // the work, or from any part of it.
32 // It is possible to build this program in a way that it loads OpenSSL
33 // libraries at run-time. If doing so, the following notices are required
34 // by the OpenSSL and SSLeay licenses:
36 // This product includes software developed by the OpenSSL Project
37 // for use in the OpenSSL Toolkit. (http://www.openssl.org/)
39 // This product includes cryptographic software written by Eric Young
40 // (eay@cryptsoft.com)
43 // The most up-to-date version of this program is always available from
44 // http://shellinabox.com
49 // The author believes that for the purposes of this license, you meet the
50 // requirements for publishing the source code, if your web server publishes
51 // the source in unmodified form (i.e. with licensing information, comments,
52 // formatting, and identifier names intact). If there are technical reasons
53 // that require you to make changes to the source code when serving the
54 // JavaScript (e.g to remove pre-processor directives from the source), these
55 // changes should be done in a reversible fashion.
57 // The author does not consider websites that reference this script in
58 // unmodified form, and web servers that serve this script in unmodified form
59 // to be derived works. As such, they are believed to be outside of the
60 // scope of this license and not subject to the rights or restrictions of the
61 // GNU General Public License.
63 // If in doubt, consult a legal professional familiar with the laws that
64 // apply in your country.
66 #define XHR_UNITIALIZED 0
69 #define XHR_RECEIVING 3
72 // IE does not define XMLHttpRequest by default, so we provide a suitable
74 if (typeof XMLHttpRequest == 'undefined') {
75 XMLHttpRequest = function() {
76 try { return new ActiveXObject('Msxml2.XMLHTTP.6.0');} catch (e) { }
77 try { return new ActiveXObject('Msxml2.XMLHTTP.3.0');} catch (e) { }
78 try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { }
79 try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) { }
84 function extend(subClass, baseClass) {
85 function inheritance() { }
86 inheritance.prototype = baseClass.prototype;
87 subClass.prototype = new inheritance();
88 subClass.prototype.constructor = subClass;
89 subClass.prototype.superClass = baseClass.prototype;
92 function ShellInABox(url, container) {
93 if (url == undefined) {
94 this.url = document.location.href.replace(/[?#].*/, '');
98 if (document.location.hash != '') {
99 var hash = decodeURIComponent(document.location.hash).
101 this.nextUrl = hash.replace(/,.*/, '');
102 this.session = hash.replace(/[^,]*,/, '');
104 this.nextUrl = this.url;
107 this.pendingKeys = '';
108 this.keysInFlight = false;
109 this.connected = false;
110 this.superClass.constructor.call(this, container);
112 // We have to initiate the first XMLHttpRequest from a timer. Otherwise,
113 // Chrome never realizes that the page has loaded.
114 setTimeout(function(shellInABox) {
116 shellInABox.sendRequest();
120 extend(ShellInABox, VT100);
122 ShellInABox.prototype.sessionClosed = function() {
124 this.connected = false;
126 this.session = undefined;
127 if (this.cursorX > 0) {
130 this.vt100('Session closed.');
132 this.showReconnect(true);
137 ShellInABox.prototype.reconnect = function() {
138 this.showReconnect(false);
140 if (document.location.hash != '') {
141 // A shellinaboxd daemon launched from a CGI only allows a single
142 // session. In order to reconnect, we must reload the frame definition
143 // and obtain a new port number. As this is a different origin, we
144 // need to get enclosing page to help us.
145 parent.location = this.nextUrl;
147 if (this.url != this.nextUrl) {
148 document.location.replace(this.nextUrl);
150 this.pendingKeys = '';
151 this.keysInFlight = false;
160 ShellInABox.prototype.sendRequest = function(request) {
161 if (request == undefined) {
162 request = new XMLHttpRequest();
164 request.open('POST', this.url + '?', true);
165 request.setRequestHeader('Cache-Control', 'no-cache');
166 request.setRequestHeader('Content-Type',
167 'application/x-www-form-urlencoded; charset=utf-8');
168 var content = 'width=' + this.terminalWidth +
169 '&height=' + this.terminalHeight +
170 (this.session ? '&session=' +
171 encodeURIComponent(this.session) : '');
172 request.setRequestHeader('Content-Length', content.length);
174 request.onreadystatechange = function(shellInABox) {
177 return shellInABox.onReadyStateChange(request);
179 shellInABox.sessionClosed();
183 request.send(content);
186 ShellInABox.prototype.onReadyStateChange = function(request) {
187 if (request.readyState == XHR_LOADED) {
188 if (request.status == 200) {
189 this.connected = true;
190 var response = eval('(' + request.responseText + ')');
192 this.vt100(response.data);
195 if (!response.session ||
196 this.session && this.session != response.session) {
197 this.sessionClosed();
199 this.session = response.session;
200 this.sendRequest(request);
202 } else if (request.status == 0) {
204 this.sendRequest(request);
206 this.sessionClosed();
211 ShellInABox.prototype.sendKeys = function(keys) {
212 if (!this.connected) {
215 if (this.keysInFlight || this.session == undefined) {
216 this.pendingKeys += keys;
218 this.keysInFlight = true;
219 keys = this.pendingKeys + keys;
220 this.pendingKeys = '';
221 var request = new XMLHttpRequest();
222 request.open('POST', this.url + '?', true);
223 request.setRequestHeader('Cache-Control', 'no-cache');
224 request.setRequestHeader('Content-Type',
225 'application/x-www-form-urlencoded; charset=utf-8');
226 var content = 'width=' + this.terminalWidth +
227 '&height=' + this.terminalHeight +
228 '&session=' +encodeURIComponent(this.session)+
229 '&keys=' + encodeURIComponent(keys);
230 request.setRequestHeader('Content-Length', content.length);
231 request.onreadystatechange = function(shellInABox) {
234 return shellInABox.keyPressReadyStateChange(request);
239 request.send(content);
243 ShellInABox.prototype.keyPressReadyStateChange = function(request) {
244 if (request.readyState == XHR_LOADED) {
245 this.keysInFlight = false;
246 if (this.pendingKeys) {
252 ShellInABox.prototype.keysPressed = function(ch) {
253 var hex = '0123456789ABCDEF';
255 for (var i = 0; i < ch.length; i++) {
256 var c = ch.charCodeAt(i);
258 s += hex.charAt(c >> 4) + hex.charAt(c & 0xF);
259 } else if (c < 0x800) {
260 s += hex.charAt(0xC + (c >> 10) ) +
261 hex.charAt( (c >> 6) & 0xF ) +
262 hex.charAt(0x8 + ((c >> 4) & 0x3)) +
263 hex.charAt( c & 0xF );
264 } else if (c < 0x10000) {
266 hex.charAt( (c >> 12) ) +
267 hex.charAt(0x8 + (c >> 10) & 0x3 ) +
268 hex.charAt( (c >> 6) & 0xF ) +
269 hex.charAt(0x8 + ((c >> 4) & 0x3)) +
270 hex.charAt( c & 0xF );
271 } else if (c < 0x110000) {
273 hex.charAt( (c >> 18) ) +
274 hex.charAt(0x8 + (c >> 16) & 0x3 ) +
275 hex.charAt( (c >> 12) & 0xF ) +
276 hex.charAt(0x8 + (c >> 10) & 0x3 ) +
277 hex.charAt( (c >> 6) & 0xF ) +
278 hex.charAt(0x8 + ((c >> 4) & 0x3)) +
279 hex.charAt( c & 0xF );
285 ShellInABox.prototype.resized = function(w, h) {
286 // Do not send a resize request until we are fully initialized.
288 // sendKeys() always transmits the current terminal size. So, flush all
294 ShellInABox.prototype.toggleSSL = function() {
295 if (document.location.hash != '') {
296 if (this.nextUrl.match(/\?plain$/)) {
297 this.nextUrl = this.nextUrl.replace(/\?plain$/, '');
299 this.nextUrl = this.nextUrl.replace(/[?#].*/, '') + '?plain';
302 parent.location = this.nextUrl;
305 this.nextUrl = this.nextUrl.match(/^https:/)
306 ? this.nextUrl.replace(/^https:/, 'http:').replace(/\/*$/, '/plain')
307 : this.nextUrl.replace(/^http/, 'https').replace(/\/*plain$/, '');
309 if (this.nextUrl.match(/^[:]*:\/\/[^/]*$/)) {
312 if (this.session && this.nextUrl != this.url) {
313 alert('This change will take effect the next time you login.');
317 ShellInABox.prototype.extendContextMenu = function(entries, actions) {
318 // Modify the entries and actions in place, adding any locally defined
320 var oldActions = [ ];
321 for (var i = 0; i < actions.length; i++) {
322 oldActions[i] = actions[i];
324 for (var node = entries.firstChild, i = 0, j = 0; node;
325 node = node.nextSibling) {
326 if (node.tagName == 'LI') {
327 actions[i++] = oldActions[j++];
328 if (node.id == "endconfig") {
330 if (typeof serverSupportsSSL != 'undefined' && serverSupportsSSL &&
331 !(typeof disableSSLMenu != 'undefined' && disableSSLMenu)) {
332 // If the server supports both SSL and plain text connections,
333 // provide a menu entry to switch between the two.
334 var newNode = document.createElement('li');
336 if (document.location.hash != '') {
337 isSecure = !this.nextUrl.match(/\?plain$/);
339 isSecure = this.nextUrl.match(/^https:/);
341 newNode.innerHTML = (isSecure ? '✔ ' : '') + 'Secure';
342 if (node.nextSibling) {
343 entries.insertBefore(newNode, node.nextSibling);
345 entries.appendChild(newNode);
347 actions[i++] = this.toggleSSL;
350 node.id = 'endconfig';
357 ShellInABox.prototype.about = function() {
358 alert("Shell In A Box version " + VERSION +
359 "\nCopyright 2008-2009 by Markus Gutschke\n" +
360 "For more information check http://shellinabox.com" +
361 (typeof serverSupportsSSL != 'undefined' && serverSupportsSSL ?
363 "This product includes software developed by the OpenSSL Project\n" +
364 "for use in the OpenSSL Toolkit. (http://www.openssl.org/)\n" +
366 "This product includes cryptographic software written by " +
367 "Eric Young\n(eay@cryptsoft.com)" :