504 lines
17 KiB
JavaScript
504 lines
17 KiB
JavaScript
"use strict";
|
|
/*
|
|
Copyright (C) 2012 by Jeremy P. White <jwhite@codeweavers.com>
|
|
|
|
This file is part of spice-html5.
|
|
|
|
spice-html5 is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Lesser General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
spice-html5 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 Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public License
|
|
along with spice-html5. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*----------------------------------------------------------------------------
|
|
** SpiceConn
|
|
** This is the base Javascript class for establishing and
|
|
** managing a connection to a Spice Server.
|
|
** It is used to provide core functionality to the Spice main,
|
|
** display, inputs, and cursor channels. See main.js for
|
|
** usage.
|
|
**--------------------------------------------------------------------------*/
|
|
function SpiceConn(o)
|
|
{
|
|
if (o === undefined || o.uri === undefined || ! o.uri)
|
|
throw new Error("You must specify a uri");
|
|
|
|
this.ws = new WebSocket(o.uri, 'binary');
|
|
|
|
if (! this.ws.binaryType)
|
|
throw new Error("WebSocket doesn't support binaryType. Try a different browser.");
|
|
|
|
this.connection_id = o.connection_id !== undefined ? o.connection_id : 0;
|
|
this.type = o.type !== undefined ? o.type : SPICE_CHANNEL_MAIN;
|
|
this.chan_id = o.chan_id !== undefined ? o.chan_id : 0;
|
|
if (o.parent !== undefined)
|
|
{
|
|
this.parent = o.parent;
|
|
this.message_id = o.parent.message_id;
|
|
this.password = o.parent.password;
|
|
}
|
|
if (o.screen_id !== undefined)
|
|
this.screen_id = o.screen_id;
|
|
if (o.dump_id !== undefined)
|
|
this.dump_id = o.dump_id;
|
|
if (o.message_id !== undefined)
|
|
this.message_id = o.message_id;
|
|
if (o.password !== undefined)
|
|
this.password = o.password;
|
|
if (o.onerror !== undefined)
|
|
this.onerror = o.onerror;
|
|
if (o.onsuccess !== undefined)
|
|
this.onsuccess = o.onsuccess;
|
|
if (o.onagent !== undefined)
|
|
this.onagent = o.onagent;
|
|
|
|
this.state = "connecting";
|
|
this.ws.parent = this;
|
|
this.wire_reader = new SpiceWireReader(this, this.process_inbound);
|
|
this.messages_sent = 0;
|
|
this.warnings = [];
|
|
|
|
this.ws.addEventListener('open', function(e) {
|
|
DEBUG > 0 && console.log(">> WebSockets.onopen");
|
|
DEBUG > 0 && console.log("id " + this.parent.connection_id +"; type " + this.parent.type);
|
|
|
|
/***********************************************************************
|
|
** WHERE IT ALL REALLY BEGINS
|
|
***********************************************************************/
|
|
this.parent.send_hdr();
|
|
this.parent.wire_reader.request(SpiceLinkHeader.prototype.buffer_size());
|
|
this.parent.state = "start";
|
|
});
|
|
this.ws.addEventListener('error', function(e) {
|
|
if ('url' in e.target) {
|
|
this.parent.log_err("WebSocket error: Can't connect to websocket on URL: " + e.target.url);
|
|
}
|
|
this.parent.report_error(e);
|
|
});
|
|
this.ws.addEventListener('close', function(e) {
|
|
DEBUG > 0 && console.log(">> WebSockets.onclose");
|
|
DEBUG > 0 && console.log("id " + this.parent.connection_id +"; type " + this.parent.type);
|
|
DEBUG > 0 && console.log(e);
|
|
if (this.parent.state != "closing" && this.parent.state != "error" && this.parent.onerror !== undefined)
|
|
{
|
|
var e;
|
|
if (this.parent.state == "connecting")
|
|
e = new Error("Connection refused.");
|
|
else if (this.parent.state == "start" || this.parent.state == "link")
|
|
e = new Error("Unexpected protocol mismatch.");
|
|
else if (this.parent.state == "ticket")
|
|
e = new Error("Bad password.");
|
|
else
|
|
e = new Error("Unexpected close while " + this.parent.state);
|
|
|
|
this.parent.onerror(e);
|
|
this.parent.log_err(e.toString());
|
|
}
|
|
});
|
|
|
|
if (this.ws.readyState == 2 || this.ws.readyState == 3)
|
|
throw new Error("Unable to connect to " + o.uri);
|
|
|
|
this.timeout = window.setTimeout(spiceconn_timeout, SPICE_CONNECT_TIMEOUT, this);
|
|
}
|
|
|
|
SpiceConn.prototype =
|
|
{
|
|
send_hdr : function ()
|
|
{
|
|
var hdr = new SpiceLinkHeader;
|
|
var msg = new SpiceLinkMess;
|
|
|
|
msg.connection_id = this.connection_id;
|
|
msg.channel_type = this.type;
|
|
msg.channel_id = this.chan_id;
|
|
|
|
msg.common_caps.push(
|
|
(1 << SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION) |
|
|
(1 << SPICE_COMMON_CAP_MINI_HEADER)
|
|
);
|
|
|
|
if (msg.channel_type == SPICE_CHANNEL_PLAYBACK)
|
|
{
|
|
var caps = 0;
|
|
if ('MediaSource' in window && MediaSource.isTypeSupported(SPICE_PLAYBACK_CODEC))
|
|
caps |= (1 << SPICE_PLAYBACK_CAP_OPUS);
|
|
msg.channel_caps.push(caps);
|
|
}
|
|
else if (msg.channel_type == SPICE_CHANNEL_MAIN)
|
|
{
|
|
msg.channel_caps.push(
|
|
(1 << SPICE_MAIN_CAP_AGENT_CONNECTED_TOKENS)
|
|
);
|
|
}
|
|
else if (msg.channel_type == SPICE_CHANNEL_DISPLAY)
|
|
{
|
|
var caps = (1 << SPICE_DISPLAY_CAP_SIZED_STREAM) |
|
|
(1 << SPICE_DISPLAY_CAP_STREAM_REPORT) |
|
|
(1 << SPICE_DISPLAY_CAP_MULTI_CODEC) |
|
|
(1 << SPICE_DISPLAY_CAP_CODEC_MJPEG);
|
|
if ('MediaSource' in window && MediaSource.isTypeSupported(SPICE_VP8_CODEC))
|
|
caps |= (1 << SPICE_DISPLAY_CAP_CODEC_VP8);
|
|
msg.channel_caps.push(caps);
|
|
}
|
|
|
|
hdr.size = msg.buffer_size();
|
|
|
|
var mb = new ArrayBuffer(hdr.buffer_size() + msg.buffer_size());
|
|
hdr.to_buffer(mb);
|
|
msg.to_buffer(mb, hdr.buffer_size());
|
|
|
|
DEBUG > 1 && console.log("Sending header:");
|
|
DEBUG > 2 && hexdump_buffer(mb);
|
|
this.ws.send(mb);
|
|
},
|
|
|
|
send_ticket: function(ticket)
|
|
{
|
|
var hdr = new SpiceLinkAuthTicket();
|
|
hdr.auth_mechanism = SPICE_COMMON_CAP_AUTH_SPICE;
|
|
// FIXME - we need to implement RSA to make this work right
|
|
hdr.encrypted_data = ticket;
|
|
var mb = new ArrayBuffer(hdr.buffer_size());
|
|
|
|
hdr.to_buffer(mb);
|
|
DEBUG > 1 && console.log("Sending ticket:");
|
|
DEBUG > 2 && hexdump_buffer(mb);
|
|
this.ws.send(mb);
|
|
},
|
|
|
|
send_msg: function(msg)
|
|
{
|
|
var mb = new ArrayBuffer(msg.buffer_size());
|
|
msg.to_buffer(mb);
|
|
this.messages_sent++;
|
|
DEBUG > 0 && console.log(">> hdr " + this.channel_type() + " type " + msg.type + " size " + mb.byteLength);
|
|
DEBUG > 2 && hexdump_buffer(mb);
|
|
this.ws.send(mb);
|
|
},
|
|
|
|
process_inbound: function(mb, saved_header)
|
|
{
|
|
DEBUG > 2 && console.log(this.type + ": processing message of size " + mb.byteLength + "; state is " + this.state);
|
|
if (this.state == "ready")
|
|
{
|
|
if (saved_header == undefined)
|
|
{
|
|
var msg = new SpiceMiniData(mb);
|
|
|
|
if (msg.type > 500)
|
|
{
|
|
if (DEBUG > 0)
|
|
{
|
|
alert("Something has gone very wrong; we think we have message of type " + msg.type);
|
|
debugger;
|
|
}
|
|
}
|
|
|
|
if (msg.size == 0)
|
|
{
|
|
this.process_message(msg);
|
|
this.wire_reader.request(SpiceMiniData.prototype.buffer_size());
|
|
}
|
|
else
|
|
{
|
|
this.wire_reader.request(msg.size);
|
|
this.wire_reader.save_header(msg);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
saved_header.data = mb;
|
|
this.process_message(saved_header);
|
|
this.wire_reader.request(SpiceMiniData.prototype.buffer_size());
|
|
this.wire_reader.save_header(undefined);
|
|
}
|
|
}
|
|
|
|
else if (this.state == "start")
|
|
{
|
|
this.reply_hdr = new SpiceLinkHeader(mb);
|
|
if (this.reply_hdr.magic != SPICE_MAGIC)
|
|
{
|
|
this.state = "error";
|
|
var e = new Error('Error: magic mismatch: ' + this.reply_hdr.magic);
|
|
this.report_error(e);
|
|
}
|
|
else
|
|
{
|
|
// FIXME - Determine major/minor version requirements
|
|
this.wire_reader.request(this.reply_hdr.size);
|
|
this.state = "link";
|
|
}
|
|
}
|
|
|
|
else if (this.state == "link")
|
|
{
|
|
this.reply_link = new SpiceLinkReply(mb);
|
|
// FIXME - Screen the caps - require minihdr at least, right?
|
|
if (this.reply_link.error)
|
|
{
|
|
this.state = "error";
|
|
var e = new Error('Error: reply link error ' + this.reply_link.error);
|
|
this.report_error(e);
|
|
}
|
|
else
|
|
{
|
|
this.send_ticket(rsa_encrypt(this.reply_link.pub_key, this.password + String.fromCharCode(0)));
|
|
this.state = "ticket";
|
|
this.wire_reader.request(SpiceLinkAuthReply.prototype.buffer_size());
|
|
}
|
|
}
|
|
|
|
else if (this.state == "ticket")
|
|
{
|
|
this.auth_reply = new SpiceLinkAuthReply(mb);
|
|
if (this.auth_reply.auth_code == SPICE_LINK_ERR_OK)
|
|
{
|
|
DEBUG > 0 && console.log(this.type + ': Connected');
|
|
|
|
if (this.type == SPICE_CHANNEL_DISPLAY)
|
|
{
|
|
// FIXME - pixmap and glz dictionary config info?
|
|
var dinit = new SpiceMsgcDisplayInit();
|
|
var reply = new SpiceMiniData();
|
|
reply.build_msg(SPICE_MSGC_DISPLAY_INIT, dinit);
|
|
DEBUG > 0 && console.log("Request display init");
|
|
this.send_msg(reply);
|
|
}
|
|
this.state = "ready";
|
|
this.wire_reader.request(SpiceMiniData.prototype.buffer_size());
|
|
if (this.timeout)
|
|
{
|
|
window.clearTimeout(this.timeout);
|
|
delete this.timeout;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.state = "error";
|
|
if (this.auth_reply.auth_code == SPICE_LINK_ERR_PERMISSION_DENIED)
|
|
{
|
|
var e = new Error("Permission denied.");
|
|
}
|
|
else
|
|
{
|
|
var e = new Error("Unexpected link error " + this.auth_reply.auth_code);
|
|
}
|
|
this.report_error(e);
|
|
}
|
|
}
|
|
},
|
|
|
|
process_common_messages : function(msg)
|
|
{
|
|
if (msg.type == SPICE_MSG_SET_ACK)
|
|
{
|
|
var ack = new SpiceMsgSetAck(msg.data);
|
|
// FIXME - what to do with generation?
|
|
this.ack_window = ack.window;
|
|
DEBUG > 1 && console.log(this.type + ": set ack to " + ack.window);
|
|
this.msgs_until_ack = this.ack_window;
|
|
var ackack = new SpiceMsgcAckSync(ack);
|
|
var reply = new SpiceMiniData();
|
|
reply.build_msg(SPICE_MSGC_ACK_SYNC, ackack);
|
|
this.send_msg(reply);
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_PING)
|
|
{
|
|
DEBUG > 1 && console.log("ping!");
|
|
var pong = new SpiceMiniData;
|
|
pong.type = SPICE_MSGC_PONG;
|
|
if (msg.data)
|
|
{
|
|
pong.data = msg.data.slice(0, 12);
|
|
}
|
|
pong.size = pong.buffer_size();
|
|
this.send_msg(pong);
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_NOTIFY)
|
|
{
|
|
// FIXME - Visibility + what
|
|
var notify = new SpiceMsgNotify(msg.data);
|
|
if (notify.severity == SPICE_NOTIFY_SEVERITY_ERROR)
|
|
this.log_err(notify.message);
|
|
else if (notify.severity == SPICE_NOTIFY_SEVERITY_WARN )
|
|
this.log_warn(notify.message);
|
|
else
|
|
this.log_info(notify.message);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
process_message: function(msg)
|
|
{
|
|
var rc;
|
|
var start = Date.now();
|
|
DEBUG > 0 && console.log("<< hdr " + this.channel_type() + " type " + msg.type + " size " + (msg.data && msg.data.byteLength));
|
|
rc = this.process_common_messages(msg);
|
|
if (! rc)
|
|
{
|
|
if (this.process_channel_message)
|
|
{
|
|
rc = this.process_channel_message(msg);
|
|
if (! rc)
|
|
this.log_warn(this.channel_type() + ": Unknown message type " + msg.type + "!");
|
|
}
|
|
else
|
|
this.log_err(this.channel_type() + ": No message handlers for this channel; message " + msg.type);
|
|
}
|
|
|
|
if (this.msgs_until_ack !== undefined && this.ack_window)
|
|
{
|
|
this.msgs_until_ack--;
|
|
if (this.msgs_until_ack <= 0)
|
|
{
|
|
this.msgs_until_ack = this.ack_window;
|
|
var ack = new SpiceMiniData();
|
|
ack.type = SPICE_MSGC_ACK;
|
|
this.send_msg(ack);
|
|
DEBUG > 1 && console.log(this.type + ": sent ack");
|
|
}
|
|
}
|
|
|
|
var delta = Date.now() - start;
|
|
if (DEBUG > 0 || delta > GAP_DETECTION_THRESHOLD)
|
|
console.log("delta " + this.channel_type() + ":" + msg.type + " " + delta);
|
|
return rc;
|
|
},
|
|
|
|
channel_type: function()
|
|
{
|
|
if (this.type == SPICE_CHANNEL_MAIN)
|
|
return "main";
|
|
else if (this.type == SPICE_CHANNEL_DISPLAY)
|
|
return "display";
|
|
else if (this.type == SPICE_CHANNEL_INPUTS)
|
|
return "inputs";
|
|
else if (this.type == SPICE_CHANNEL_CURSOR)
|
|
return "cursor";
|
|
else if (this.type == SPICE_CHANNEL_PLAYBACK)
|
|
return "playback";
|
|
else if (this.type == SPICE_CHANNEL_RECORD)
|
|
return "record";
|
|
else if (this.type == SPICE_CHANNEL_TUNNEL)
|
|
return "tunnel";
|
|
else if (this.type == SPICE_CHANNEL_SMARTCARD)
|
|
return "smartcard";
|
|
else if (this.type == SPICE_CHANNEL_USBREDIR)
|
|
return "usbredir";
|
|
else if (this.type == SPICE_CHANNEL_PORT)
|
|
return "port";
|
|
else if (this.type == SPICE_CHANNEL_WEBDAV)
|
|
return "webdav";
|
|
return "unknown-" + this.type;
|
|
|
|
},
|
|
|
|
log_info: function()
|
|
{
|
|
var msg = Array.prototype.join.call(arguments, " ");
|
|
console.log(msg);
|
|
if (this.message_id)
|
|
{
|
|
var p = document.createElement("p");
|
|
p.appendChild(document.createTextNode(msg));
|
|
p.className += "spice-message-info";
|
|
document.getElementById(this.message_id).appendChild(p);
|
|
}
|
|
},
|
|
|
|
log_warn: function()
|
|
{
|
|
var msg = Array.prototype.join.call(arguments, " ");
|
|
console.log("WARNING: " + msg);
|
|
if (this.message_id)
|
|
{
|
|
var p = document.createElement("p");
|
|
p.appendChild(document.createTextNode(msg));
|
|
p.className += "spice-message-warning";
|
|
document.getElementById(this.message_id).appendChild(p);
|
|
}
|
|
},
|
|
|
|
log_err: function()
|
|
{
|
|
var msg = Array.prototype.join.call(arguments, " ");
|
|
console.log("ERROR: " + msg);
|
|
if (this.message_id)
|
|
{
|
|
var p = document.createElement("p");
|
|
p.appendChild(document.createTextNode(msg));
|
|
p.className += "spice-message-error";
|
|
document.getElementById(this.message_id).appendChild(p);
|
|
}
|
|
},
|
|
|
|
known_unimplemented: function(type, msg)
|
|
{
|
|
if ( (!this.warnings[type]) || DEBUG > 1)
|
|
{
|
|
var str = "";
|
|
if (DEBUG <= 1)
|
|
str = " [ further notices suppressed ]";
|
|
this.log_warn("Unimplemented function " + type + "(" + msg + ")" + str);
|
|
this.warnings[type] = true;
|
|
}
|
|
},
|
|
|
|
report_error: function(e)
|
|
{
|
|
this.log_err(e.toString());
|
|
if (this.onerror != undefined)
|
|
this.onerror(e);
|
|
else
|
|
throw(e);
|
|
},
|
|
|
|
report_success: function(m)
|
|
{
|
|
if (this.onsuccess != undefined)
|
|
this.onsuccess(m);
|
|
},
|
|
|
|
cleanup: function()
|
|
{
|
|
if (this.timeout)
|
|
{
|
|
window.clearTimeout(this.timeout);
|
|
delete this.timeout;
|
|
}
|
|
if (this.ws)
|
|
{
|
|
this.ws.close();
|
|
this.ws = undefined;
|
|
}
|
|
},
|
|
|
|
handle_timeout: function()
|
|
{
|
|
var e = new Error("Connection timed out.");
|
|
this.report_error(e);
|
|
},
|
|
}
|
|
|
|
function spiceconn_timeout(sc)
|
|
{
|
|
SpiceConn.prototype.handle_timeout.call(sc);
|
|
}
|