[Security] Address critical flaw in console rendering that allowed arbitrary command execution

This commit is contained in:
Dane Everitt 2017-06-26 22:36:09 -05:00
parent ddb98df4af
commit 829453f805
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
11 changed files with 515 additions and 548 deletions

View file

@ -3,6 +3,13 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines. This project follows [Semantic Versioning](http://semver.org) guidelines.
## v0.6.3 (Courageous Carniadactylus)
### Fixed
* **[Security]** — Addresses an oversight in how the terminal rendered information sent from the server feed which allowed a malicious user to execute arbitrary commands on the game-server process itself by using a specifically crafted in-game command.
### Changed
* Removed `jquery.terminal` and replaced it with an in-house developed terminal with less potential for security issues.
## v0.6.2 (Courageous Carniadactylus) ## v0.6.2 (Courageous Carniadactylus)
### Fixed ### Fixed
* Fixes a few typos throughout the panel, there are more don't worry. * Fixes a few typos throughout the panel, there are more don't worry.

View file

@ -39,6 +39,8 @@ AdminLTE - [license](https://github.com/almasaeed2010/AdminLTE/blob/master/LICEN
Animate.css - [license](https://github.com/daneden/animate.css/blob/master/LICENSE) - [homepage](http://daneden.github.io/animate.css/) Animate.css - [license](https://github.com/daneden/animate.css/blob/master/LICENSE) - [homepage](http://daneden.github.io/animate.css/)
AnsiUp - [license](https://github.com/drudru/ansi_up/blob/master/Readme.md#license) - [homepage](https://github.com/drudru/ansi_up)
Async.js - [license](https://github.com/caolan/async/blob/master/LICENSE) - [homepage](https://github.com/caolan/async/) Async.js - [license](https://github.com/caolan/async/blob/master/LICENSE) - [homepage](https://github.com/caolan/async/)
Bootstrap - [license](https://github.com/twbs/bootstrap/blob/master/LICENSE) - [homepage](http://getbootstrap.com) Bootstrap - [license](https://github.com/twbs/bootstrap/blob/master/LICENSE) - [homepage](http://getbootstrap.com)
@ -53,8 +55,6 @@ FontAwesome Animations - [license](https://github.com/l-lin/font-awesome-animati
jQuery - [license](https://github.com/jquery/jquery/blob/master/LICENSE.txt) - [homepage](http://jquery.com) jQuery - [license](https://github.com/jquery/jquery/blob/master/LICENSE.txt) - [homepage](http://jquery.com)
jQuery Terminal - [license](https://github.com/jcubic/jquery.terminal/blob/master/LICENSE) - [homepage](http://terminal.jcubic.pl)
Laravel Framework - [license](https://github.com/laravel/framework/blob/5.4/LICENSE.md) - [homepage](https://laravel.com) Laravel Framework - [license](https://github.com/laravel/framework/blob/5.4/LICENSE.md) - [homepage](https://laravel.com)
Lodash - [license](https://github.com/lodash/lodash/blob/master/LICENSE) - [homepage](https://lodash.com/) Lodash - [license](https://github.com/lodash/lodash/blob/master/LICENSE) - [homepage](https://lodash.com/)

View file

@ -311,3 +311,90 @@ input.form-autocomplete-stop[readonly] {
background: white; background: white;
box-shadow: none !important; box-shadow: none !important;
} }
#terminal {
font-family: monospace;
color: #aaa;
background: #000;
font-size: 12px;
line-height: 14px;
padding: 10px 10px 0;
box-sizing: border-box;
min-height: 30px;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
border-radius: 5px 5px 0 0;
}
#terminal > .cmd {
padding: 1px 0;
}
@keyframes blinky {
0% {
background: transparent;
}
100% {
background: rgba(170, 170, 170, 0.9);
}
}
@-webkit-keyframes blinky {
0% {
background: transparent;
}
100% {
background: rgba(170, 170, 170, 0.9);
}
}
#terminal_input {
width: 100%;
background: #000;
border-radius: 0 0 5px 5px;
padding: 5px 10px;
}
.terminal_input--input {
height: 0;
width: 0;
position: absolute;
top: -20px;
}
.terminal_input--text, .terminal_input--prompt {
line-height: 14px;
width: 100%;
vertical-align: middle;
font-size: 12px;
font-family: monospace;
margin-bottom: 0;
background: transparent;
color: #aaa;
}
.terminal_input--text:before, .terminal_input--text:after {
content: "";
display: inline-block;
width: 7px;
height: 14px;
margin: 0 0 -12px -6px;
vertical-align: middle;
}
.terminal_input--text:after {
position: relative;
bottom: 8px;
left: 8px;
background: #ff00;
animation: blinky 0.6s linear infinite alternate;
-webkit-animation: blinky 0.6s linear infinite alternate;
}
.terminal_input--input {
color: transparent;
background-color: transparent;
border: 0;
outline: none;
}

View file

@ -20,39 +20,56 @@
var CONSOLE_PUSH_COUNT = Pterodactyl.config.console_count || 10; var CONSOLE_PUSH_COUNT = Pterodactyl.config.console_count || 10;
var CONSOLE_PUSH_FREQ = Pterodactyl.config.console_freq || 200; var CONSOLE_PUSH_FREQ = Pterodactyl.config.console_freq || 200;
var CONSOLE_OUTPUT_LIMIT = Pterodactyl.config.console_limit || 2000; var CONSOLE_OUTPUT_LIMIT = Pterodactyl.config.console_limit || 2000;
var InitialLogSent = false; var InitialLogSent = false;
var AnsiUp = new AnsiUp;
var $terminal = $('#terminal');
var $ghostInput = $('.terminal_input--input');
var $visibleInput = $('.terminal_input--text');
var $scrollNotify = $('#terminalNotify');
$(document).ready(function () {
$ghostInput.focus();
$('.terminal_input--text, #terminal_input, #terminal, #terminalNotify').on('click', function () {
$ghostInput.focus();
});
$ghostInput.on('input', function () {
$visibleInput.html($(this).val());
});
$ghostInput.on('keyup', function (e) {
if (e.which === 13) {
Socket.emit((ConsoleServerStatus !== 0) ? 'send command' : 'set status', $(this).val());
$(this).val('');
$visibleInput.html('');
}
});
});
$terminal.on('scroll', function () {
if ($(this).scrollTop() + $(this).innerHeight() < $(this)[0].scrollHeight) {
$scrollNotify.removeClass('hidden');
} else {
$scrollNotify.addClass('hidden');
}
});
window.scrollToBottom = function () {
$terminal.scrollTop($terminal[0].scrollHeight);
};
(function initConsole() { (function initConsole() {
window.TerminalQueue = []; window.TerminalQueue = [];
window.ConsoleServerStatus = 0; window.ConsoleServerStatus = 0;
window.Terminal = $('#terminal').terminal(function (command, term) { window.ConsoleElements = 0;
Socket.emit((ConsoleServerStatus !== 0) ? 'send command' : 'set status', command);
}, { $scrollNotify.on('click', function () {
greetings: '', window.scrollToBottom();
name: Pterodactyl.server.uuid, $scrollNotify.addClass('hidden');
height: 450,
exit: false,
echoCommand: false,
outputLimit: CONSOLE_OUTPUT_LIMIT,
prompt: Pterodactyl.server.username + ':~$ ',
scrollOnEcho: false,
scrollBottomOffset: 5,
onBlur: function (terminal) {
return false;
}
}); });
window.TerminalNotifyElement = $('#terminalNotify');
TerminalNotifyElement.on('click', function () {
Terminal.scroll_to_bottom();
TerminalNotifyElement.addClass('hidden');
})
Terminal.on('scroll', function () {
if (Terminal.is_bottom()) {
TerminalNotifyElement.addClass('hidden');
}
})
})(); })();
(function pushOutputQueue() { (function pushOutputQueue() {
@ -62,16 +79,22 @@ var InitialLogSent = false;
if (TerminalQueue.length > 0) { if (TerminalQueue.length > 0) {
for (var i = 0; i < CONSOLE_PUSH_COUNT && TerminalQueue.length > 0; i++) { for (var i = 0; i < CONSOLE_PUSH_COUNT && TerminalQueue.length > 0; i++) {
Terminal.echo(TerminalQueue[0], { flush: false }); $terminal.append(
'<div class="cmd">' + AnsiUp.ansi_to_html(TerminalQueue[0] + '\u001b[0m') + '</div>'
);
if (! $scrollNotify.is(':visible')) {
window.scrollToBottom();
}
window.ConsoleElements++;
TerminalQueue.shift(); TerminalQueue.shift();
} }
// Flush after looping through all. var removeElements = window.ConsoleElements - CONSOLE_OUTPUT_LIMIT;
Terminal.flush(); if (removeElements > 0) {
$('#terminal').find('.cmd').slice(0, removeElements).remove();
// Show Warning window.ConsoleElements = window.ConsoleElements - removeElements;
if (! Terminal.is_bottom()) {
TerminalNotifyElement.removeClass('hidden');
} }
} }
@ -99,14 +122,18 @@ var InitialLogSent = false;
Socket.on('server log', function (data) { Socket.on('server log', function (data) {
if (! InitialLogSent) { if (! InitialLogSent) {
Terminal.clear(); $('#terminal').html('');
TerminalQueue.push(data); data.split(/\n/g).forEach(function (item) {
TerminalQueue.push(item);
});
InitialLogSent = true; InitialLogSent = true;
} }
}); });
Socket.on('console', function (data) { Socket.on('console', function (data) {
TerminalQueue.push(data.line); data.line.split(/\n/g).forEach(function (item) {
TerminalQueue.push(item);
});
}); });
})(); })();

View file

@ -0,0 +1,335 @@
/* ansi_up.js
* author : Dru Nelson
* license : MIT
* http://github.com/drudru/ansi_up
*/
(function (factory) {
var v;
if (typeof module === "object" && typeof module.exports === "object") {
v = factory(require, exports);
if ("undefined" !== typeof v) module.exports = v;
}
else if ("function" === typeof define && define.amd) {
define(["require", "exports"], factory);
}
else {
var req, exp = {};
v = factory(req, exp);
window.AnsiUp = exp.default;
}
})(function (require, exports) {
"use strict";
function rgx(tmplObj) {
var subst = [];
for (var _i = 1; _i < arguments.length; _i++) {
subst[_i - 1] = arguments[_i];
}
var regexText = tmplObj.raw[0];
var wsrgx = /^\s+|\s+\n|\s+#[\s\S]+?\n/gm;
var txt2 = regexText.replace(wsrgx, '');
return new RegExp(txt2, 'm');
}
var AnsiUp = (function () {
function AnsiUp() {
this.VERSION = "2.0.1";
this.ansi_colors = [
[
{ rgb: [0, 0, 0], class_name: "ansi-black" },
{ rgb: [187, 0, 0], class_name: "ansi-red" },
{ rgb: [0, 187, 0], class_name: "ansi-green" },
{ rgb: [187, 187, 0], class_name: "ansi-yellow" },
{ rgb: [0, 0, 187], class_name: "ansi-blue" },
{ rgb: [187, 0, 187], class_name: "ansi-magenta" },
{ rgb: [0, 187, 187], class_name: "ansi-cyan" },
{ rgb: [255, 255, 255], class_name: "ansi-white" }
],
[
{ rgb: [85, 85, 85], class_name: "ansi-bright-black" },
{ rgb: [255, 85, 85], class_name: "ansi-bright-red" },
{ rgb: [0, 255, 0], class_name: "ansi-bright-green" },
{ rgb: [255, 255, 85], class_name: "ansi-bright-yellow" },
{ rgb: [85, 85, 255], class_name: "ansi-bright-blue" },
{ rgb: [255, 85, 255], class_name: "ansi-bright-magenta" },
{ rgb: [85, 255, 255], class_name: "ansi-bright-cyan" },
{ rgb: [255, 255, 255], class_name: "ansi-bright-white" }
]
];
this.htmlFormatter = {
transform: function (fragment, instance) {
var txt = fragment.text;
if (txt.length === 0)
return txt;
if (instance._escape_for_html)
txt = instance.old_escape_for_html(txt);
if (!fragment.bright && fragment.fg === null && fragment.bg === null)
return txt;
var styles = [];
var classes = [];
var fg = fragment.fg;
var bg = fragment.bg;
if (fg === null && fragment.bright)
fg = instance.ansi_colors[1][7];
if (!instance._use_classes) {
if (fg)
styles.push("color:rgb(" + fg.rgb.join(',') + ")");
if (bg)
styles.push("background-color:rgb(" + bg.rgb + ")");
}
else {
if (fg) {
if (fg.class_name !== 'truecolor') {
classes.push(fg.class_name + "-fg");
}
else {
styles.push("color:rgb(" + fg.rgb.join(',') + ")");
}
}
if (bg) {
if (bg.class_name !== 'truecolor') {
classes.push(bg.class_name + "-bg");
}
else {
styles.push("background-color:rgb(" + bg.rgb.join(',') + ")");
}
}
}
var class_string = '';
var style_string = '';
if (classes.length)
class_string = " class=\"" + classes.join(' ') + "\"";
if (styles.length)
style_string = " style=\"" + styles.join(';') + "\"";
return "<span" + class_string + style_string + ">" + txt + "</span>";
},
compose: function (segments, instance) {
return segments.join("");
}
};
this.textFormatter = {
transform: function (fragment, instance) {
return fragment.text;
},
compose: function (segments, instance) {
return segments.join("");
}
};
this.setup_256_palette();
this._use_classes = false;
this._escape_for_html = true;
this.bright = false;
this.fg = this.bg = null;
this._buffer = '';
}
Object.defineProperty(AnsiUp.prototype, "use_classes", {
get: function () {
return this._use_classes;
},
set: function (arg) {
this._use_classes = arg;
},
enumerable: true,
configurable: true
});
Object.defineProperty(AnsiUp.prototype, "escape_for_html", {
get: function () {
return this._escape_for_html;
},
set: function (arg) {
this._escape_for_html = arg;
},
enumerable: true,
configurable: true
});
AnsiUp.prototype.setup_256_palette = function () {
var _this = this;
this.palette_256 = [];
this.ansi_colors.forEach(function (palette) {
palette.forEach(function (rec) {
_this.palette_256.push(rec);
});
});
var levels = [0, 95, 135, 175, 215, 255];
for (var r = 0; r < 6; ++r) {
for (var g = 0; g < 6; ++g) {
for (var b = 0; b < 6; ++b) {
var col = { rgb: [levels[r], levels[g], levels[b]], class_name: 'truecolor' };
this.palette_256.push(col);
}
}
}
var grey_level = 8;
for (var i = 0; i < 24; ++i, grey_level += 10) {
var gry = { rgb: [grey_level, grey_level, grey_level], class_name: 'truecolor' };
this.palette_256.push(gry);
}
};
AnsiUp.prototype.old_escape_for_html = function (txt) {
return txt.replace(/[&<>]/gm, function (str) {
if (str === "&")
return "&amp;";
if (str === "<")
return "&lt;";
if (str === ">")
return "&gt;";
});
};
AnsiUp.prototype.old_linkify = function (txt) {
return txt.replace(/(https?:\/\/[^\s]+)/gm, function (str) {
return "<a href=\"" + str + "\">" + str + "</a>";
});
};
AnsiUp.prototype.detect_incomplete_ansi = function (txt) {
return !(/.*?[\x40-\x7e]/.test(txt));
};
AnsiUp.prototype.detect_incomplete_link = function (txt) {
var found = false;
for (var i = txt.length - 1; i > 0; i--) {
if (/\s|\x1B/.test(txt[i])) {
found = true;
break;
}
}
if (!found) {
if (/(https?:\/\/[^\s]+)/.test(txt))
return 0;
else
return -1;
}
var prefix = txt.substr(i + 1, 4);
if (prefix.length === 0)
return -1;
if ("http".indexOf(prefix) === 0)
return (i + 1);
};
AnsiUp.prototype.ansi_to = function (txt, formatter) {
var pkt = this._buffer + txt;
this._buffer = '';
var raw_text_pkts = pkt.split(/\x1B\[/);
if (raw_text_pkts.length === 1)
raw_text_pkts.push('');
this.handle_incomplete_sequences(raw_text_pkts);
var first_chunk = this.with_state(raw_text_pkts.shift());
var blocks = new Array(raw_text_pkts.length);
for (var i = 0, len = raw_text_pkts.length; i < len; ++i) {
blocks[i] = (formatter.transform(this.process_ansi(raw_text_pkts[i]), this));
}
if (first_chunk.text.length > 0)
blocks.unshift(formatter.transform(first_chunk, this));
return formatter.compose(blocks, this);
};
AnsiUp.prototype.ansi_to_html = function (txt) {
return this.ansi_to(txt, this.htmlFormatter);
};
AnsiUp.prototype.ansi_to_text = function (txt) {
return this.ansi_to(txt, this.textFormatter);
};
AnsiUp.prototype.with_state = function (text) {
return { bright: this.bright, fg: this.fg, bg: this.bg, text: text };
};
AnsiUp.prototype.handle_incomplete_sequences = function (chunks) {
var last_chunk = chunks[chunks.length - 1];
if ((last_chunk.length > 0) && this.detect_incomplete_ansi(last_chunk)) {
this._buffer = "\x1B[" + last_chunk;
chunks.pop();
chunks.push('');
}
else {
if (last_chunk.slice(-1) === "\x1B") {
this._buffer = "\x1B";
console.log("raw", chunks);
chunks.pop();
chunks.push(last_chunk.substr(0, last_chunk.length - 1));
console.log(chunks);
console.log(last_chunk);
}
if (chunks.length === 2 &&
chunks[1] === "" &&
chunks[0].slice(-1) === "\x1B") {
this._buffer = "\x1B";
last_chunk = chunks.shift();
chunks.unshift(last_chunk.substr(0, last_chunk.length - 1));
}
}
};
AnsiUp.prototype.process_ansi = function (block) {
if (!this._sgr_regex) {
this._sgr_regex = (_a = ["\n ^ # beginning of line\n ([!<-?]?) # a private-mode char (!, <, =, >, ?)\n ([d;]*) # any digits or semicolons\n ([ -/]? # an intermediate modifier\n [@-~]) # the command\n ([sS]*) # any text following this CSI sequence\n "], _a.raw = ["\n ^ # beginning of line\n ([!\\x3c-\\x3f]?) # a private-mode char (!, <, =, >, ?)\n ([\\d;]*) # any digits or semicolons\n ([\\x20-\\x2f]? # an intermediate modifier\n [\\x40-\\x7e]) # the command\n ([\\s\\S]*) # any text following this CSI sequence\n "], rgx(_a));
}
var matches = block.match(this._sgr_regex);
if (!matches) {
return this.with_state(block);
}
var orig_txt = matches[4];
if (matches[1] !== '' || matches[3] !== 'm') {
return this.with_state(orig_txt);
}
var sgr_cmds = matches[2].split(';');
while (sgr_cmds.length > 0) {
var sgr_cmd_str = sgr_cmds.shift();
var num = parseInt(sgr_cmd_str, 10);
if (isNaN(num) || num === 0) {
this.fg = this.bg = null;
this.bright = false;
}
else if (num === 1) {
this.bright = true;
}
else if (num === 22) {
this.bright = false;
}
else if (num === 39) {
this.fg = null;
}
else if (num === 49) {
this.bg = null;
}
else if ((num >= 30) && (num < 38)) {
var bidx = this.bright ? 1 : 0;
this.fg = this.ansi_colors[bidx][(num - 30)];
}
else if ((num >= 90) && (num < 98)) {
this.fg = this.ansi_colors[1][(num - 90)];
}
else if ((num >= 40) && (num < 48)) {
this.bg = this.ansi_colors[0][(num - 40)];
}
else if ((num >= 100) && (num < 108)) {
this.bg = this.ansi_colors[1][(num - 100)];
}
else if (num === 38 || num === 48) {
if (sgr_cmds.length > 0) {
var is_foreground = (num === 38);
var mode_cmd = sgr_cmds.shift();
if (mode_cmd === '5' && sgr_cmds.length > 0) {
var palette_index = parseInt(sgr_cmds.shift(), 10);
if (palette_index >= 0 && palette_index <= 255) {
if (is_foreground)
this.fg = this.palette_256[palette_index];
else
this.bg = this.palette_256[palette_index];
}
}
if (mode_cmd === '2' && sgr_cmds.length > 2) {
var r = parseInt(sgr_cmds.shift(), 10);
var g = parseInt(sgr_cmds.shift(), 10);
var b = parseInt(sgr_cmds.shift(), 10);
if ((r >= 0 && r <= 255) && (g >= 0 && g <= 255) && (b >= 0 && b <= 255)) {
var c = { rgb: [r, g, b], class_name: 'truecolor' };
if (is_foreground)
this.fg = c;
else
this.bg = c;
}
}
}
}
}
return this.with_state(orig_txt);
var _a;
};
return AnsiUp;
}());
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = AnsiUp;
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,121 +0,0 @@
/* global define, KeyboardEvent, module */
// https://github.com/cvan/keyboardevent-key-polyfill/blob/master/LICENSE.md
(function () {
var keyboardeventKeyPolyfill = {
polyfill: polyfill,
keys: {
3: 'Cancel',
6: 'Help',
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
16: 'Shift',
17: 'Control',
18: 'Alt',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
28: 'Convert',
29: 'NonConvert',
30: 'Accept',
31: 'ModeChange',
32: ' ',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
41: 'Select',
42: 'Print',
43: 'Execute',
44: 'PrintScreen',
45: 'Insert',
46: 'Delete',
48: ['0', ')'],
49: ['1', '!'],
50: ['2', '@'],
51: ['3', '#'],
52: ['4', '$'],
53: ['5', '%'],
54: ['6', '^'],
55: ['7', '&'],
56: ['8', '*'],
57: ['9', '('],
91: 'OS',
93: 'ContextMenu',
144: 'NumLock',
145: 'ScrollLock',
181: 'VolumeMute',
182: 'VolumeDown',
183: 'VolumeUp',
186: [';', ':'],
187: ['=', '+'],
188: [',', '<'],
189: ['-', '_'],
190: ['.', '>'],
191: ['/', '?'],
192: ['`', '~'],
219: ['[', '{'],
220: ['\\', '|'],
221: [']', '}'],
222: ["'", '"'],
224: 'Meta',
225: 'AltGraph',
246: 'Attn',
247: 'CrSel',
248: 'ExSel',
249: 'EraseEof',
250: 'Play',
251: 'ZoomOut'
}
};
// Function keys (F1-24).
var i;
for (i = 1; i < 25; i++) {
keyboardeventKeyPolyfill.keys[111 + i] = 'F' + i;
}
// Printable ASCII characters.
var letter = '';
for (i = 65; i < 91; i++) {
letter = String.fromCharCode(i);
keyboardeventKeyPolyfill.keys[i] = [letter.toLowerCase(), letter.toUpperCase()];
}
function polyfill () {
if (!('KeyboardEvent' in window) ||
'key' in KeyboardEvent.prototype) {
return false;
}
// Polyfill `key` on `KeyboardEvent`.
var proto = {
get: function (x) {
var key = keyboardeventKeyPolyfill.keys[this.which || this.keyCode];
if (Array.isArray(key)) {
key = key[+this.shiftKey];
}
return key;
}
};
Object.defineProperty(KeyboardEvent.prototype, 'key', proto);
return proto;
}
if (typeof define === 'function' && define.amd) {
define('keyboardevent-key-polyfill', keyboardeventKeyPolyfill);
} else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
module.exports = keyboardeventKeyPolyfill;
} else if (window) {
window.keyboardeventKeyPolyfill = keyboardeventKeyPolyfill;
}
})();

View file

@ -1,318 +0,0 @@
/**@license
* __ _____ ________ __
* / // _ /__ __ _____ ___ __ _/__ ___/__ ___ ______ __ __ __ ___ / /
* __ / // // // // // _ // _// // / / // _ // _// // // \/ // _ \/ /
* / / // // // // // ___// / / // / / // ___// / / / / // // /\ // // / /__
* \___//____ \\___//____//_/ _\_ / /_//____//_/ /_/ /_//_//_/ /_/ \__\_\___/
* \/ /____/
* http://terminal.jcubic.pl
*
* This is example of how to create custom formatter for jQuery Terminal
*
* Copyright (c) 2014-2016 Jakub Jankiewicz <http://jcubic.pl/me>
* Released under the MIT license
*
*/
(function($) {
if (!$.terminal) {
throw new Error('$.terminal is not defined');
}
// ---------------------------------------------------------------------
// :: Replace overtyping (from man) formatting with terminal formatting
// ---------------------------------------------------------------------
$.terminal.overtyping = function(string) {
return string.replace(/((?:_\x08.|.\x08_)+)/g, function(full) {
var striped = full.replace(/_x08|\x08_|_\u0008|\u0008_/g, '');
return '[[u;;]' + striped + ']';
}).replace(/((?:.\x08.)+)/g, function(full) {
return '[[b;#fff;]' + full.replace(/(.)(?:\x08|\u0008)\1/g,
function(full, g) {
return g;
}) + ']';
});
};
// ---------------------------------------------------------------------
// :: Html colors taken from ANSI formatting in Linux Terminal
// ---------------------------------------------------------------------
$.terminal.ansi_colors = {
normal: {
black: '#000',
red: '#A00',
green: '#008400',
yellow: '#A50',
blue: '#00A',
magenta: '#A0A',
cyan: '#0AA',
white: '#AAA'
},
faited: {
black: '#000',
red: '#640000',
green: '#006100',
yellow: '#737300',
blue: '#000087',
magenta: '#650065',
cyan: '#008787',
white: '#818181'
},
bold: {
black: '#000',
red: '#F55',
green: '#44D544',
yellow: '#FF5',
blue: '#55F',
magenta: '#F5F',
cyan: '#5FF',
white: '#FFF'
},
// XTerm 8-bit pallete
palette: [
'#000000', '#AA0000', '#00AA00', '#AA5500', '#0000AA', '#AA00AA',
'#00AAAA', '#AAAAAA', '#555555', '#FF5555', '#55FF55', '#FFFF55',
'#5555FF', '#FF55FF', '#55FFFF', '#FFFFFF', '#000000', '#00005F',
'#000087', '#0000AF', '#0000D7', '#0000FF', '#005F00', '#005F5F',
'#005F87', '#005FAF', '#005FD7', '#005FFF', '#008700', '#00875F',
'#008787', '#0087AF', '#0087D7', '#0087FF', '#00AF00', '#00AF5F',
'#00AF87', '#00AFAF', '#00AFD7', '#00AFFF', '#00D700', '#00D75F',
'#00D787', '#00D7AF', '#00D7D7', '#00D7FF', '#00FF00', '#00FF5F',
'#00FF87', '#00FFAF', '#00FFD7', '#00FFFF', '#5F0000', '#5F005F',
'#5F0087', '#5F00AF', '#5F00D7', '#5F00FF', '#5F5F00', '#5F5F5F',
'#5F5F87', '#5F5FAF', '#5F5FD7', '#5F5FFF', '#5F8700', '#5F875F',
'#5F8787', '#5F87AF', '#5F87D7', '#5F87FF', '#5FAF00', '#5FAF5F',
'#5FAF87', '#5FAFAF', '#5FAFD7', '#5FAFFF', '#5FD700', '#5FD75F',
'#5FD787', '#5FD7AF', '#5FD7D7', '#5FD7FF', '#5FFF00', '#5FFF5F',
'#5FFF87', '#5FFFAF', '#5FFFD7', '#5FFFFF', '#870000', '#87005F',
'#870087', '#8700AF', '#8700D7', '#8700FF', '#875F00', '#875F5F',
'#875F87', '#875FAF', '#875FD7', '#875FFF', '#878700', '#87875F',
'#878787', '#8787AF', '#8787D7', '#8787FF', '#87AF00', '#87AF5F',
'#87AF87', '#87AFAF', '#87AFD7', '#87AFFF', '#87D700', '#87D75F',
'#87D787', '#87D7AF', '#87D7D7', '#87D7FF', '#87FF00', '#87FF5F',
'#87FF87', '#87FFAF', '#87FFD7', '#87FFFF', '#AF0000', '#AF005F',
'#AF0087', '#AF00AF', '#AF00D7', '#AF00FF', '#AF5F00', '#AF5F5F',
'#AF5F87', '#AF5FAF', '#AF5FD7', '#AF5FFF', '#AF8700', '#AF875F',
'#AF8787', '#AF87AF', '#AF87D7', '#AF87FF', '#AFAF00', '#AFAF5F',
'#AFAF87', '#AFAFAF', '#AFAFD7', '#AFAFFF', '#AFD700', '#AFD75F',
'#AFD787', '#AFD7AF', '#AFD7D7', '#AFD7FF', '#AFFF00', '#AFFF5F',
'#AFFF87', '#AFFFAF', '#AFFFD7', '#AFFFFF', '#D70000', '#D7005F',
'#D70087', '#D700AF', '#D700D7', '#D700FF', '#D75F00', '#D75F5F',
'#D75F87', '#D75FAF', '#D75FD7', '#D75FFF', '#D78700', '#D7875F',
'#D78787', '#D787AF', '#D787D7', '#D787FF', '#D7AF00', '#D7AF5F',
'#D7AF87', '#D7AFAF', '#D7AFD7', '#D7AFFF', '#D7D700', '#D7D75F',
'#D7D787', '#D7D7AF', '#D7D7D7', '#D7D7FF', '#D7FF00', '#D7FF5F',
'#D7FF87', '#D7FFAF', '#D7FFD7', '#D7FFFF', '#FF0000', '#FF005F',
'#FF0087', '#FF00AF', '#FF00D7', '#FF00FF', '#FF5F00', '#FF5F5F',
'#FF5F87', '#FF5FAF', '#FF5FD7', '#FF5FFF', '#FF8700', '#FF875F',
'#FF8787', '#FF87AF', '#FF87D7', '#FF87FF', '#FFAF00', '#FFAF5F',
'#FFAF87', '#FFAFAF', '#FFAFD7', '#FFAFFF', '#FFD700', '#FFD75F',
'#FFD787', '#FFD7AF', '#FFD7D7', '#FFD7FF', '#FFFF00', '#FFFF5F',
'#FFFF87', '#FFFFAF', '#FFFFD7', '#FFFFFF', '#080808', '#121212',
'#1C1C1C', '#262626', '#303030', '#3A3A3A', '#444444', '#4E4E4E',
'#585858', '#626262', '#6C6C6C', '#767676', '#808080', '#8A8A8A',
'#949494', '#9E9E9E', '#A8A8A8', '#B2B2B2', '#BCBCBC', '#C6C6C6',
'#D0D0D0', '#DADADA', '#E4E4E4', '#EEEEEE'
]
};
// ---------------------------------------------------------------------
// :: Replace ANSI formatting with terminal formatting
// ---------------------------------------------------------------------
$.terminal.from_ansi = (function() {
var color_list = {
30: 'black',
31: 'red',
32: 'green',
33: 'yellow',
34: 'blue',
35: 'magenta',
36: 'cyan',
37: 'white',
39: 'inherit' // default color
};
var background_list = {
40: 'black',
41: 'red',
42: 'green',
43: 'yellow',
44: 'blue',
45: 'magenta',
46: 'cyan',
47: 'white',
49: 'transparent' // default background
};
function format_ansi(code) {
var controls = code.split(';');
var num;
var faited = false;
var reverse = false;
var bold = false;
var styles = [];
var output_color = '';
var output_background = '';
var _8bit_color = false;
var _8bit_background = false;
var process_8bit = false;
var palette = $.terminal.ansi_colors.palette;
function set_styles(num) {
switch (num) {
case 1:
styles.push('b');
bold = true;
faited = false;
break;
case 4:
styles.push('u');
break;
case 3:
styles.push('i');
break;
case 5:
process_8bit = true;
break;
case 38:
_8bit_color = true;
break;
case 48:
_8bit_background = true;
break;
case 2:
faited = true;
bold = false;
break;
case 7:
reverse = true;
break;
default:
if (controls.indexOf('5') === -1) {
if (color_list[num]) {
output_color = color_list[num];
}
if (background_list[num]) {
output_background = background_list[num];
}
}
}
}
for (var i in controls) {
if (controls.hasOwnProperty(i)) {
num = parseInt(controls[i], 10);
if (process_8bit && (_8bit_background || _8bit_color)) {
if (_8bit_color && palette[num]) {
output_color = palette[num];
}
if (_8bit_background && palette[num]) {
output_background = palette[num];
}
} else {
set_styles(num);
}
}
}
if (reverse) {
if (output_color || output_background) {
var tmp = output_background;
output_background = output_color;
output_color = tmp;
} else {
output_color = 'black';
output_background = 'white';
}
}
var colors, color, background, backgrounds;
if (bold) {
colors = backgrounds = $.terminal.ansi_colors.bold;
} else if (faited) {
colors = backgrounds = $.terminal.ansi_colors.faited;
} else {
colors = backgrounds = $.terminal.ansi_colors.normal;
}
if (_8bit_color) {
color = output_color;
} else if (output_color === 'inherit') {
color = output_color;
} else {
color = colors[output_color];
}
if (_8bit_background) {
background = output_background;
} else if (output_background === 'transparent') {
background = output_background;
} else {
background = backgrounds[output_background];
}
return [styles.join(''), color, background];
}
return function(input) {
//merge multiple codes
/*input = input.replace(/((?:\x1B\[[0-9;]*[A-Za-z])*)/g, function(group) {
return group.replace(/m\x1B\[/g, ';');
});*/
var splitted = input.split(/(\x1B\[[0-9;]*[A-Za-z])/g);
if (splitted.length === 1) {
return input;
}
var output = [];
//skip closing at the begining
if (splitted.length > 3) {
var str = splitted.slice(0, 3).join('');
if (str.match(/^\[0*m$/)) {
splitted = splitted.slice(3);
}
}
var prev_color, prev_background, code, match;
var inside = false;
for (var i = 0; i < splitted.length; ++i) {
match = splitted[i].match(/^\x1B\[([0-9;]*)([A-Za-z])$/);
if (match) {
switch (match[2]) {
case 'm':
if (+match[1] !== 0) {
code = format_ansi(match[1]);
}
if (inside) {
output.push(']');
if (+match[1] === 0) {
//just closing
inside = false;
prev_color = prev_background = '';
} else {
// someone forget to close - move to next
code[1] = code[1] || prev_color;
code[2] = code[2] || prev_background;
output.push('[[' + code.join(';') + ']');
// store colors to next use
if (code[1]) {
prev_color = code[1];
}
if (code[2]) {
prev_background = code[2];
}
}
} else if (+match[1] !== 0) {
inside = true;
code[1] = code[1] || prev_color;
code[2] = code[2] || prev_background;
output.push('[[' + code.join(';') + ']');
// store colors to next use
if (code[1]) {
prev_color = code[1];
}
if (code[2]) {
prev_background = code[2];
}
}
break;
}
} else {
output.push(splitted[i]);
}
}
if (inside) {
output.push(']');
}
return output.join(''); //.replace(/\[\[[^\]]+\]\]/g, '');
};
})();
$.terminal.defaults.formatters.push($.terminal.overtyping);
$.terminal.defaults.formatters.push($.terminal.from_ansi);
})(jQuery);

View file

@ -22,28 +22,36 @@
<head> <head>
<title>{{ Settings::get('company', 'Pterodactyl') }} - Console &rarr; {{ $server->name }}</title> <title>{{ Settings::get('company', 'Pterodactyl') }} - Console &rarr; {{ $server->name }}</title>
@include('layouts.scripts') @include('layouts.scripts')
{!! Theme::css('vendor/terminal/jquery.terminal.css') !!} {!! Theme::css('vendor/bootstrap/bootstrap.min.css') !!}
{!! Theme::css('css/pterodactyl.css') !!}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head> </head>
<body style="margin:0;width:100%;height:100%;"> <body style="margin:0;width:100%;height:100%;background:#000;overflow: hidden;">
<div id="terminal" style="width:100%"></div> <div id="terminal" style="width:100%;max-height: none !important;"></div>
<div id="terminal_input">
<span class="terminal_input--prompt">{{ $server->username }}:~$</span> <span class="terminal_input--text"></span>
<input type="text" class="terminal_input--input" />
</div>
<div id="terminalNotify" class="terminal-notify hidden"> <div id="terminalNotify" class="terminal-notify hidden">
<i class="fa fa-bell"></i> <i class="fa fa-bell"></i>
</div> </div>
</body> </body>
<script>window.SkipConsoleCharts = true</script> <script>window.SkipConsoleCharts = true</script>
{!! Theme::js('js/laroute.js') !!} {!! Theme::js('js/laroute.js') !!}
{!! Theme::js('vendor/ansi/ansi_up.js') !!}
{!! Theme::js('vendor/jquery/jquery.min.js') !!} {!! Theme::js('vendor/jquery/jquery.min.js') !!}
{!! Theme::js('vendor/socketio/socket.io.min.js') !!} {!! Theme::js('vendor/socketio/socket.io.min.js') !!}
{!! Theme::js('vendor/bootstrap-notify/bootstrap-notify.min.js') !!} {!! Theme::js('vendor/bootstrap-notify/bootstrap-notify.min.js') !!}
{!! Theme::js('js/frontend/server.socket.js') !!} {!! Theme::js('js/frontend/server.socket.js') !!}
{!! Theme::js('vendor/mousewheel/jquery.mousewheel-min.js') !!} {!! Theme::js('vendor/mousewheel/jquery.mousewheel-min.js') !!}
{!! Theme::js('vendor/terminal/jquery.terminal.min.js') !!}
{!! Theme::js('vendor/terminal/unix_formatting.js') !!}
{!! Theme::js('js/frontend/console.js') !!} {!! Theme::js('js/frontend/console.js') !!}
<script> <script>
Terminal.resize($(window).innerWidth() - 20, $(window).innerHeight() - 20); $terminal.height($(window).innerHeight() - 40);
$terminal.width($(window).innerWidth() - 40);
$(window).on('resize', function () { $(window).on('resize', function () {
Terminal.resize($(window).innerWidth() - 20, $(window).innerHeight() - 20); window.scrollToBottom();
$terminal.height($(window).innerHeight() - 40);
$terminal.width($(window).innerWidth() - 40);
}); });
</script> </script>
</html> </html>

View file

@ -23,11 +23,6 @@
{{ trans('server.index.title', [ 'name' => $server->name]) }} {{ trans('server.index.title', [ 'name' => $server->name]) }}
@endsection @endsection
@section('scripts')
@parent
{!! Theme::css('vendor/terminal/jquery.terminal.css') !!}
@endsection
@section('content-header') @section('content-header')
<h1>@lang('server.index.header')<small>@lang('server.index.header_sub')</small></h1> <h1>@lang('server.index.header')<small>@lang('server.index.header_sub')</small></h1>
<ol class="breadcrumb"> <ol class="breadcrumb">
@ -42,6 +37,10 @@
<div class="box"> <div class="box">
<div class="box-body position-relative"> <div class="box-body position-relative">
<div id="terminal" style="width:100%;"></div> <div id="terminal" style="width:100%;"></div>
<div id="terminal_input">
<span class="terminal_input--prompt">{{ $server->username }}:~$</span> <span class="terminal_input--text"></span>
<input type="text" class="terminal_input--input" />
</div>
<div id="terminalNotify" class="terminal-notify hidden"> <div id="terminalNotify" class="terminal-notify hidden">
<i class="fa fa-bell"></i> <i class="fa fa-bell"></i>
</div> </div>
@ -81,10 +80,9 @@
@section('footer-scripts') @section('footer-scripts')
@parent @parent
{!! Theme::js('vendor/ansi/ansi_up.js') !!}
{!! Theme::js('js/frontend/server.socket.js') !!} {!! Theme::js('js/frontend/server.socket.js') !!}
{!! Theme::js('vendor/mousewheel/jquery.mousewheel-min.js') !!} {!! Theme::js('vendor/mousewheel/jquery.mousewheel-min.js') !!}
{!! Theme::js('vendor/terminal/jquery.terminal.min.js') !!}
{!! Theme::js('vendor/terminal/unix_formatting.js') !!}
{!! Theme::js('js/frontend/console.js') !!} {!! Theme::js('js/frontend/console.js') !!}
{!! Theme::js('vendor/chartjs/chart.min.js') !!} {!! Theme::js('vendor/chartjs/chart.min.js') !!}
{!! Theme::js('vendor/jquery/date-format.min.js') !!} {!! Theme::js('vendor/jquery/date-format.min.js') !!}