--- /dev/null
+[submodule "xtermjs"]
+ path = xtermjs
+ url = ../mirror_xterm.js
--- /dev/null
+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}
--- /dev/null
+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
+
--- /dev/null
+pve-xtermjs (0.1-1~rc1) unstable; urgency=medium
+
+ * initial package
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 20 Nov 2017 08:25:32 +0100
--- /dev/null
+Source: pve-xtermjs
+Section: web
+Priority: optional
+Maintainer: Proxmox Support Team <support@proxmox.com>
+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
--- /dev/null
+Copyright (C) 2017 Proxmox Server Solutions GmbH
+
+This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
+
+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 <http://www.gnu.org/licenses/>.
+
+-------------
+
+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.
--- /dev/null
+debian/SOURCE
--- /dev/null
+#!/usr/bin/make -f
+
+%:
+ dh $@
--- /dev/null
+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}
--- /dev/null
+include ../defines.mk
+
+all:
+
+.PHONY: install
+install:
+ make -C bin install
+ make -C PVE install
+ make -C www install
--- /dev/null
+include ../../../defines.mk
+
+all:
+
+.PHONY: install
+install:
+ install -d ${PERLLIBDIR}/PVE/CLI
+ install -m 0644 termproxy.pm ${PERLLIBDIR}/PVE/CLI/
--- /dev/null
+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;
--- /dev/null
+.PHONY: install
+install:
+ make -C CLI install
--- /dev/null
+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}
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use PVE::AccessControl;
+
+use PVE::CLI::termproxy;
+
+PVE::CLI::termproxy->run_cli_handler();
--- /dev/null
+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
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <title>[% nodename %] - Proxmox Console</title>
+ <link rel="stylesheet" href="/xtermjs/xterm.css?version=@VERSION@" />
+ <link rel="stylesheet" href="/xtermjs/style.css?version=@VERSION@" />
+ <script src="/xtermjs/xterm.js?version=@VERSION@" ></script>
+ <script src="/xtermjs/fit.js?version=@VERSION@" ></script>
+ <script src="/xtermjs/util.js?version=@VERSION@" ></script>
+ </head>
+ <body>
+ <div id="status_bar"></div>
+ <div id="wrap">
+ <div id="terminal-container"></div>
+ </div>
+ <script type="text/javascript">
+ if (typeof(PVE) === 'undefined') PVE = {};
+ PVE.UserName = '[% username %]';
+ PVE.CSRFPreventionToken = '[% token %]';
+ </script>
+ <script src="/xtermjs/main.js?version=@VERSION@" defer ></script>
+ </body>
+</html>
--- /dev/null
+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);
+}
--- /dev/null
+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);
+}
--- /dev/null
+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";
+ }
+}
--- /dev/null
+Subproject commit ea07bf8f694a6e9714779b19c174e26162c39196