"use strict"; /* Copyright (C) 2012 by Jeremy P. White 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 . */ /*---------------------------------------------------------------------------- ** 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); }