From: Dominik Csapak Date: Wed, 22 Nov 2017 15:45:34 +0000 (+0100) Subject: initial commit X-Git-Url: https://git.proxmox.com/?p=pve-xtermjs.git;a=commitdiff_plain;h=dcf3d43b8a3d2dad782527adead6c3c62675e571 initial commit working version of xtermjs client and termproxy as authentication proxy between a command/socket and a tcp socket Signed-off-by: Dominik Csapak --- dcf3d43b8a3d2dad782527adead6c3c62675e571 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a22f66b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "xtermjs"] + path = xtermjs + url = ../mirror_xterm.js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f16e464 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +include defines.mk + +XTERMJSDIR=xtermjs +SRCDIR=src + +ARCH:=$(shell dpkg-architecture -qDEB_BUILD_ARCH) +GITVERSION:=$(shell cat .git/refs/heads/master) + +DEB=${PACKAGE}_${VERSION}_${ARCH}.deb + +all: ${DEB} + @echo ${DEB} + +.PHONY: deb +deb: ${DEB} +${DEB}: | submodule + rm -rf ${SRCDIR}.tmp + cp -rpa ${SRCDIR} ${SRCDIR}.tmp + cp -a debian ${SRCDIR}.tmp/ + cp -ar ${XTERMJSDIR}/dist/* ${SRCDIR}.tmp/www + echo "git clone git://git.proxmox.com/git/pve-xtermjs.git\\ngit checkout ${GITVERSION}" > ${SRCDIR}.tmp/debian/SOURCE + cd ${SRCDIR}.tmp; dpkg-buildpackage -rfakeroot -b -uc -us + lintian ${DEB} + @echo ${DEB} + +.PHONY: submodule +submodule: + test -f "${XTERMJSDIR}/README.md" || git submodule update --init + +.PHONY: download +download ${SRCDIR}: + git submodule foreach 'git pull --ff-only origin master' + +.PHONY: upload +upload: ${DEB} + tar cf - ${DEB}|ssh -X repoman@repo.proxmox.com -- upload --product pmg,pve --dist stretch + +.PHONY: distclean +distclean: clean + +.PHONY: clean +clean: + rm -rf *~ debian/*~ *_${ARCH}.deb ${SRCDIR}.tmp *_all.deb *.changes *.dsc *.buildinfo + +.PHONY: dinstall +dinstall: deb + dpkg -i ${DEB} diff --git a/README b/README new file mode 100644 index 0000000..61063ca --- /dev/null +++ b/README @@ -0,0 +1,32 @@ +xterm.js webclient and helper utility +===================================== + +This repository contains the client and helper utility to use +xterm.js (https://xtermjs.org) for Proxmox VE. + +To be able to relay between the gui and a shell program/console, +we need a tool (called termproxy) to open a port (where our websocketproxy +connects to) and to open a pty and execute a program + +From Client to Server it implements a simple packet-based protocol: +(everything is a string) + +* Normal Message + 0:LENGTH:MSG + where LENGTH is the bytelength of the msg + +* Resize Message + 1:COLS:ROWS: + where COLS is the number of columns the client wants to resize to, + and ROWS the number of rows, respectively + +* Ping Message + 2 + used to keep the connection between client and server alive + (we have a timeout of 5 minutes) + +every other input from the client will be ignored + +From server to client, the data will simply sent, without any +format + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..536e568 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +pve-xtermjs (0.1-1~rc1) unstable; urgency=medium + + * initial package + + -- Proxmox Support Team Mon, 20 Nov 2017 08:25:32 +0100 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..8c1d7d1 --- /dev/null +++ b/debian/control @@ -0,0 +1,15 @@ +Source: pve-xtermjs +Section: web +Priority: optional +Maintainer: Proxmox Support Team +Build-Depends: debhelper (>= 7.0.0~) +Standards-Version: 3.8.3 + +Package: pve-xtermjs +Architecture: any +Depends: dtach, + libpve-access-control (>= 5.0-7), + libpve-common-perl (>= 5.0-22), + ${misc:Depends} +Description: HTML/JS Shell client + This is an xterm.js client for PVE Host, Container and Qemu Serial Terminal diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..79de700 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,41 @@ +Copyright (C) 2017 Proxmox Server Solutions GmbH + +This software is written by Proxmox Server Solutions GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +------------- + +xterm.js + +Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com) +Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/) + +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. diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..8696672 --- /dev/null +++ b/debian/docs @@ -0,0 +1 @@ +debian/SOURCE diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..2d33f6a --- /dev/null +++ b/debian/rules @@ -0,0 +1,4 @@ +#!/usr/bin/make -f + +%: + dh $@ diff --git a/defines.mk b/defines.mk new file mode 100644 index 0000000..1dcef10 --- /dev/null +++ b/defines.mk @@ -0,0 +1,8 @@ +PACKAGE=pve-xtermjs +VER=0.1 +PKGREL=1~rc1 +VERSION=${VER}-${PKGREL} + +BINDIR=${DESTDIR}/usr/bin +PERLLIBDIR=${DESTDIR}/usr/share/perl5 +WWWBASEDIR=${DESTDIR}/usr/share/${PACKAGE} diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..f1f27ce --- /dev/null +++ b/src/Makefile @@ -0,0 +1,9 @@ +include ../defines.mk + +all: + +.PHONY: install +install: + make -C bin install + make -C PVE install + make -C www install diff --git a/src/PVE/CLI/Makefile b/src/PVE/CLI/Makefile new file mode 100644 index 0000000..79b0db1 --- /dev/null +++ b/src/PVE/CLI/Makefile @@ -0,0 +1,8 @@ +include ../../../defines.mk + +all: + +.PHONY: install +install: + install -d ${PERLLIBDIR}/PVE/CLI + install -m 0644 termproxy.pm ${PERLLIBDIR}/PVE/CLI/ diff --git a/src/PVE/CLI/termproxy.pm b/src/PVE/CLI/termproxy.pm new file mode 100644 index 0000000..4832b12 --- /dev/null +++ b/src/PVE/CLI/termproxy.pm @@ -0,0 +1,220 @@ +package PVE::CLI::termproxy; + +use strict; +use warnings; + +use PVE::RPCEnvironment; +use PVE::CLIHandler; +use PVE::JSONSchema qw(get_standard_option); +use PVE::AccessControl; +use PVE::PTY; +use IO::Select; +use IO::Socket::IP; + +use base qw(PVE::CLIHandler); + +use constant MAX_QUEUE_LEN => 16*1024; + +sub setup_environment { + PVE::RPCEnvironment->setup_default_cli_env(); +} + +sub listen_and_authenticate { + my ($port, $timeout) = @_; + + my $params = { + Listen => 1, + ReuseAddr => 1, + Proto => &Socket::IPPROTO_TCP, + GetAddrInfoFlags => 0, + LocalAddr => 'localhost', + LocalPort => $port, + }; + + my $socket = IO::Socket::IP->new(%$params) or die "failed to open socket: $!\n"; + + alarm 0; + local $SIG{ALRM} = sub { die "timed out waiting for client\n" }; + alarm $timeout; + my $client = $socket->accept; # Wait for a client + alarm 0; + close($socket); + + my $queue; + my $n = sysread($client, $queue, 4096); + if ($n && $queue =~ s/^([^:]+):([^:]+):(.+)\n//) { + my $user = $1; + my $path = $2; + my $ticket = $3; + + die "authentication failed\n" + if !PVE::AccessControl::verify_vnc_ticket($ticket, $user, $path); + + die "aknowledge failed\n" + if !syswrite($client, "OK"); + + } else { + die "malformed authentication string\n"; + } + + return ($queue, $client); +} + +sub run_pty { + my ($cmd, $webhandle, $queue) = @_; + + foreach my $k (keys %ENV) { + next if $k eq 'PATH' || $k eq 'USER' || $k eq 'HOME' || $k eq 'LANG' || $k eq 'LANGUAGE'; + next if $k =~ m/^LC_/; + delete $ENV{$k}; + } + + $ENV{TERM} = 'xterm-256color'; + + my $pty = PVE::PTY->new(); + + my $pid = fork(); + die "fork: $!\n" if !defined($pid); + if (!$pid) { + $pty->make_controlling_terminal(); + exec {$cmd->[0]} @$cmd + or POSIX::_exit(1); + } + + $pty->set_size(80,20); + + read_write_loop($webhandle, $pty->master, $queue, $pty); + + $pty->close(); + waitpid($pid,0); + exit(0); +} + +sub read_write_loop { + my ($webhandle, $cmdhandle, $queue, $pty) = @_; + + my $select = new IO::Select; + + $select->add($webhandle); + $select->add($cmdhandle); + + my @handles; + + # we may have already messages from the first read + $queue = process_queue($queue, $cmdhandle); + + my $timeout = 5*60; + + while($select->count && scalar(@handles = $select->can_read($timeout))) { + foreach my $h (@handles) { + my $buf; + my $n = $h->sysread($buf, 4096); + + if ($h == $webhandle) { + if ($n && (length($queue) + $n) < MAX_QUEUE_LEN) { + $queue = process_queue($queue.$buf, $cmdhandle, $pty); + } else { + return; + } + } elsif ($h == $cmdhandle) { + if ($n) { + syswrite($webhandle, $buf); + } else { + return; + } + } + } + } +} + +sub process_queue { + my ($queue, $handle, $pty) = @_; + + my $msg; + while(length($queue)) { + ($queue, $msg) = remove_message($queue, $pty); + last if !defined($msg); + syswrite($handle, $msg); + } + return $queue; +} + + +# we try to remove a whole message +# if we succeed, we return the remaining queue and the msg +# if we fail, the message is undef and the queue is not changed +sub remove_message { + my ($queue, $pty) = @_; + + my $msg; + my $type = substr $queue, 0, 1; + + if ($type eq '0') { + # normal message + my ($length) = $queue =~ m/^0:(\d+):/; + my $begin = 3 + length($length); + if (defined($length) && length($queue) >= ($length + $begin)) { + $msg = substr $queue, $begin, $length; + if (defined($msg)) { + # msg contains now $length chars after 0:$length: + $queue = substr $queue, $begin + $length; + } + } + } elsif ($type eq '1') { + # resize message + my ($cols, $rows) = $queue =~ m/^1:(\d+):(\d+):/; + if (defined($cols) && defined($rows)) { + $queue = substr $queue, (length($cols) + length ($rows) + 4); + eval { $pty->set_size($cols, $rows) if defined($pty) }; + warn $@ if $@; + $msg = ""; + } + } elsif ($type eq '2') { + # ping + $queue = substr $queue, 1; + $msg = ""; + } else { + # ignore other input + $queue = substr $queue, 1; + $msg = ""; + } + + return ($queue, $msg); +} + +__PACKAGE__->register_method ({ + name => 'exec', + path => 'exec', + method => 'POST', + description => "Connects a TCP Socket with a commandline", + parameters => { + additionalProperties => 0, + properties => { + port => { + type => 'integer', + description => "The port to listen on." + }, + 'extra-args' => get_standard_option('extra-args'), + }, + }, + returns => { type => 'null'}, + code => sub { + my ($param) = @_; + + my $cmd; + if (defined($param->{'extra-args'})) { + $cmd = [@{$param->{'extra-args'}}]; + } else { + die "No command given\n"; + } + + my ($queue, $handle) = listen_and_authenticate($param->{port}, 10); + + run_pty($cmd, $handle, $queue); + + return undef; + }}); + +our $cmddef = [ __PACKAGE__, 'exec', ['port', 'extra-args' ]]; + +1; diff --git a/src/PVE/Makefile b/src/PVE/Makefile new file mode 100644 index 0000000..b0321d7 --- /dev/null +++ b/src/PVE/Makefile @@ -0,0 +1,3 @@ +.PHONY: install +install: + make -C CLI install diff --git a/src/bin/Makefile b/src/bin/Makefile new file mode 100644 index 0000000..6a6ed1c --- /dev/null +++ b/src/bin/Makefile @@ -0,0 +1,7 @@ +include ../../defines.mk + +.PHONY: install +install: termproxy + perl -I.. -T -e "use PVE::CLI::termproxy; PVE::CLI::termproxy->verify_api();" + install -d ${BINDIR} + install -m 0755 termproxy ${BINDIR} diff --git a/src/bin/termproxy b/src/bin/termproxy new file mode 100755 index 0000000..6af56c6 --- /dev/null +++ b/src/bin/termproxy @@ -0,0 +1,10 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use PVE::AccessControl; + +use PVE::CLI::termproxy; + +PVE::CLI::termproxy->run_cli_handler(); diff --git a/src/www/Makefile b/src/www/Makefile new file mode 100644 index 0000000..99914fa --- /dev/null +++ b/src/www/Makefile @@ -0,0 +1,20 @@ +include ../../defines.mk + +SOURCE = \ + addons/fit/fit.js \ + index.html.tpl \ + main.js \ + style.css \ + util.js \ + xterm.css \ + xterm.js \ + xterm.js.map + +index.html.tpl: index.html.tpl.in + sed -e 's/@VERSION@/${VERSION}/' $< >$@.tmp + mv $@.tmp $@ + +.PHONY: install +install: ${SOURCE} + install -d ${WWWBASEDIR} + set -e && for i in ${SOURCE}; do install -m 0644 $$i ${WWWBASEDIR}; done diff --git a/src/www/index.html.tpl.in b/src/www/index.html.tpl.in new file mode 100644 index 0000000..8cc74ed --- /dev/null +++ b/src/www/index.html.tpl.in @@ -0,0 +1,23 @@ + + + + [% nodename %] - Proxmox Console + + + + + + + +
+
+
+
+ + + + diff --git a/src/www/main.js b/src/www/main.js new file mode 100644 index 0000000..a489937 --- /dev/null +++ b/src/www/main.js @@ -0,0 +1,184 @@ +console.log('xtermjs: starting'); + +var states = { + start: 1, + connecting: 2, + connected: 3, + disconnecting: 4, + disconnected: 5, +}; + +var term, + protocol, + socketURL, + socket, + ticket, + path, + resize, + ping, + state = states.start; + +function updateState(newState, msg) { + var timeout, severity, message; + switch (newState) { + case states.connecting: + message = "Connecting..."; + timeout = 0; + severity = severities.warning; + break; + case states.connected: + message = "Connected"; + break; + case states.disconnecting: + message = "Disconnecting..."; + timeout = 0; + severity = severities.warning; + break; + case states.disconnected: + switch (state) { + case states.start: + case states.connecting: + message = "Connection failed"; + timeout = 0; + severity = severities.error; + break; + case states.connected: + case states.disconnecting: + message = "Connection closed"; + timeout = 0; + break; + case states.disconnected: + // no state change + break; + default: + throw "unknown state"; + } + break; + default: + throw "unknown state"; + } + if (msg) { + message += " (" + msg + ")"; + } + state = newState; + showMsg(message, timeout, severity); +} + +var terminalContainer = document.getElementById('terminal-container'); +document.getElementById('status_bar').addEventListener('click', hideMsg); + +createTerminal(); + +function createTerminal() { + term = new Terminal(); + + term.on('resize', function (size) { + if (state === states.connected) { + socket.send("1:" + size.cols + ":" + size.rows + ":"); + } + }); + + protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; + + var params = {}; + var type = getQueryParameter('console'); + var vmid = getQueryParameter('vmid'); + var vmname = getQueryParameter('vmname'); + var nodename = getQueryParameter('node'); + var url = '/nodes/' + nodename; + switch (type) { + case 'kvm': + url += '/qemu/' + vmid; + path = '/vms/' + vmid; + break; + case 'lxc': + url += '/lxc/' + vmid; + path = '/vms/' + vmid; + break; + case 'shell': + path = '/nodes/' + nodename; + break; + case 'upgrade': + params.upgrade = 1; + path = '/nodes/' + nodename; + break; + } + API2Request({ + method: 'POST', + params: params, + url: url + '/termproxy', + success: function(result) { + var port = encodeURIComponent(result.data.port); + ticket = result.data.ticket; + socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/api2/json' + url + '/vncwebsocket?port=' + port + '&vncticket=' + encodeURIComponent(ticket); + + term.open(terminalContainer, true); + socket = new WebSocket(socketURL, 'binary'); + socket.binaryType = 'arraybuffer'; + socket.onopen = runTerminal; + socket.onclose = stopTerminal; + socket.onerror = errorTerminal; + window.onbeforeunload = stopTerminal; + updateState(states.connecting); + }, + failure: function(msg) { + updateState(states.disconnected,msg); + } + }); + +} + +function runTerminal() { + socket.onmessage = function(event) { + var answer = Utf8ArrayToStr(event.data); + if (state === states.connected) { + term.write(answer); + } else if(state === states.connecting) { + if (answer.slice(0,2) === "OK") { + updateState(states.connected); + term.write(answer.slice(2)); + } else { + socket.close(); + } + } + }; + + term.on('data', function(data) { + if (state === states.connected) { + socket.send("0:" + unescape(encodeURIComponent(data)).length.toString() + ":" + data); + } + }); + + ping = setInterval(function() { + socket.send("2"); + }, 30*1000); + + window.addEventListener('resize', function() { + clearTimeout(resize); + resize = setTimeout(function() { + // done resizing + term.fit(); + }, 250); + }); + + socket.send(PVE.UserName + ':' + path + ':' + ticket + "\n"); + + setTimeout(function() {term.fit();}, 250); +} + +function stopTerminal(event) { + term.off('resize'); + term.off('data'); + clearInterval(ping); + socket.close(); + updateState(states.disconnected, event.msg + event.code); +} + +function errorTerminal(event) { + term.off('resize'); + term.off('data'); + clearInterval(ping); + socket.close(); + term.destroy(); + updateState(states.disconnected, event.msg + event.code); +} diff --git a/src/www/style.css b/src/www/style.css new file mode 100644 index 0000000..20959c0 --- /dev/null +++ b/src/www/style.css @@ -0,0 +1,85 @@ +html,body { + height: 100%; + min-height: 100%; + margin: 0; + padding: 0; + overflow: hidden; + font-family: Consolas,"DejaVu Sans Mono","Liberation Mono",Courier,monospace; + background-color: #101010; +} + +.terminal { + background-color: #101010; + color: #f0f0f0; + font-size: 10pt; + font-family: Consolas,"DejaVu Sans Mono","Liberation Mono",Courier,monospace; + font-variant-ligatures: none; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +.terminal .xterm-viewport { + background-color: rgba(121, 121, 121, 0); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + transition: background-color 800ms linear; +} + +/* fix line height on firefox */ +.xterm-rows > div > span { + display: inline-block; +} + +#terminal-container { + height: 100%; + width: auto; +} + +#wrap { + height: 100%; + width: auto; + box-sizing: border-box; + padding: 5px; +} + +#status_bar { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 500; + transform: translateY(-100%); + + transition: 0.25s ease-in-out; + + visibility: hidden; + opacity: 0; + + padding: 5px; + + display: flex; + flex-direction: row; + justify-content: center; + align-content: center; + + line-height: 25px; + color: #fff; + + border-bottom: 1px solid rgba(0, 0, 0, 0.9); +} + +#status_bar.open { + transform: translateY(0); + visibility: visible; + opacity: 1; +} + +#status_bar.normal { + background: rgba(128,128,128,0.9); +} +#status_bar.error { + background: rgba(200,55,55,0.9); +} +#status_bar.warning { + background: rgba(180,180,30,0.9); +} diff --git a/src/www/util.js b/src/www/util.js new file mode 100644 index 0000000..b2c40e3 --- /dev/null +++ b/src/www/util.js @@ -0,0 +1,181 @@ +function urlEncode(object) { + var i,value, params = []; + + for (i in object) { + if (object.hasOwnProperty(i)) { + value = object[i]; + if (value === undefined) value = ''; + params.push(encodeURIComponent(i) + '=' + encodeURIComponent(String(value))); + } + } + + return params.join('&'); +} + +var msgtimeout; +var severities = { + normal: 1, + warning: 2, + error: 3, +}; + +function showMsg(message, timeout, severity) { + var status_bar = document.getElementById('status_bar'); + clearTimeout(msgtimeout); + + status_bar.classList.remove('normal'); + status_bar.classList.remove('warning'); + status_bar.classList.remove('error'); + + status_bar.textContent = message; + + severity = severity || severities.normal; + + switch (severity) { + case severities.normal: + status_bar.classList.add('normal'); + break; + case severities.warning: + status_bar.classList.add('warning'); + break; + case severities.error: + status_bar.classList.add('error'); + break; + default: + throw "unknown severity"; + } + + status_bar.classList.add('open'); + + if (timeout !== 0) { + msgtimeout = setTimeout(hideMsg, timeout || 1500); + } +} + +function hideMsg() { + clearTimeout(msgtimeout); + status_bar.classList.remove('open'); +} + +function getQueryParameter(name) { + var params = location.search.slice(1).split('&'); + var result = ""; + params.forEach(function(param) { + var components = param.split('='); + if (components[0] === name) { + result = components.slice(1).join('='); + } + }); + return result; +} + +var cur = 0; +var left = 0; + +function Utf8ArrayToStr(arraybuffer) { + var array = new Uint8Array(arraybuffer), + i = 0, + len = array.byteLength, + out = "", + c; + + while (i < len) { + c = array[i++]; + if (!left && c < 0x80) { + out += String.fromCharCode(c); + } else if(!left) { + switch (c >> 4) { + case 12: case 13: + // 110x xxxx 10xx xxxx + cur = (c & 0x1F) << 6; + left = 1; + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + cur = (c & 0x0F) << 12; + left = 2; + break; + case 15: + // 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx + cur = (c & 0x07) << 18; + left = 3; + break; + default: + cur = 0; + out += '\ufffd'; + } + } else if (c >= 0x80 && c <= 0xBF) { + cur = cur | ((c & 0x3F) << (--left * 6)); + if (!left) { + out += String.fromCharCode(cur); + cur = 0; + } + } else { + cur = 0; + left = 0; + out += '\ufffd'; + } + } + + return out; +} + +function API2Request(reqOpts) { + var me = this; + + reqOpts.method = reqOpts.method || 'GET'; + + var xhr = new XMLHttpRequest(); + + xhr.onload = function() { + var scope = reqOpts.scope || this; + var result; + var errmsg; + + if (xhr.readyState === 4) { + var ctype = xhr.getResponseHeader('Content-Type'); + if (xhr.status === 200) { + if (ctype.match(/application\/json;/)) { + result = JSON.parse(xhr.responseText); + } else { + errmsg = 'got unexpected content type ' + ctype; + } + } else { + errmsg = 'Error ' + xhr.status + ': ' + xhr.statusText; + } + } else { + errmsg = 'Connection error - server offline?'; + } + + if (errmsg !== undefined) { + if (reqOpts.failure) { + reqOpts.failure.call(scope, errmsg); + } + } else { + if (reqOpts.success) { + reqOpts.success.call(scope, result); + } + } + if (reqOpts.callback) { + reqOpts.callback.call(scope, errmsg === undefined); + } + } + + var data = urlEncode(reqOpts.params || {}); + + if (reqOpts.method === 'GET') { + xhr.open(reqOpts.method, "/api2/json" + reqOpts.url + '?' + data); + } else { + xhr.open(reqOpts.method, "/api2/json" + reqOpts.url); + } + xhr.setRequestHeader('Cache-Control', 'no-cache'); + if (reqOpts.method === 'POST' || reqOpts.method === 'PUT') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.setRequestHeader('CSRFPreventionToken', PVE.CSRFPreventionToken); + xhr.send(data); + } else if (reqOpts.method === 'GET') { + xhr.send(); + } else { + throw "unknown method"; + } +} diff --git a/xtermjs b/xtermjs new file mode 160000 index 0000000..ea07bf8 --- /dev/null +++ b/xtermjs @@ -0,0 +1 @@ +Subproject commit ea07bf8f694a6e9714779b19c174e26162c39196