1*418db63cSGunnar Mills/** 2*418db63cSGunnar Mills * Controller for virtual-media 3*418db63cSGunnar Mills * 4*418db63cSGunnar Mills * @module app/serverControl 5*418db63cSGunnar Mills * @exports virtualMediaController 6*418db63cSGunnar Mills * @name virtualMediaController 7*418db63cSGunnar Mills */ 8*418db63cSGunnar Mills 9*418db63cSGunnar Millswindow.angular && (function(angular) { 10*418db63cSGunnar Mills 'use strict'; 11*418db63cSGunnar Mills 12*418db63cSGunnar Mills angular.module('app.serverControl').controller('virtualMediaController', [ 13*418db63cSGunnar Mills '$scope', 'APIUtils', 'toastService', 'dataService', 'nbdServerService', 14*418db63cSGunnar Mills function($scope, APIUtils, toastService, dataService, nbdServerService) { 15*418db63cSGunnar Mills $scope.devices = []; 16*418db63cSGunnar Mills 17*418db63cSGunnar Mills // Only one Virtual Media WebSocket device is currently available. 18*418db63cSGunnar Mills // Path is /vm/0/0. 19*418db63cSGunnar Mills // TODO: Support more than 1 VM device, when backend support is added. 20*418db63cSGunnar Mills var vmDevice = {}; 21*418db63cSGunnar Mills // Hardcode to 0 since /vm/0/0. Last 0 is the device ID. 22*418db63cSGunnar Mills // To support more than 1 device ID, replace with a call to get the 23*418db63cSGunnar Mills // device IDs and names. 24*418db63cSGunnar Mills vmDevice.id = 0; 25*418db63cSGunnar Mills vmDevice.deviceName = 'Virtual media device'; 26*418db63cSGunnar Mills findExistingConnection(vmDevice); 27*418db63cSGunnar Mills $scope.devices.push(vmDevice); 28*418db63cSGunnar Mills 29*418db63cSGunnar Mills $scope.startVM = function(index) { 30*418db63cSGunnar Mills $scope.devices[index].isActive = true; 31*418db63cSGunnar Mills var file = $scope.devices[index].file; 32*418db63cSGunnar Mills var id = $scope.devices[index].id; 33*418db63cSGunnar Mills var host = dataService.getHost().replace('https://', ''); 34*418db63cSGunnar Mills var server = new NBDServer('wss://' + host + '/vm/0/' + id, file, id); 35*418db63cSGunnar Mills $scope.devices[index].nbdServer = server; 36*418db63cSGunnar Mills nbdServerService.addConnection(id, server, file); 37*418db63cSGunnar Mills server.start(); 38*418db63cSGunnar Mills }; 39*418db63cSGunnar Mills $scope.stopVM = function(index) { 40*418db63cSGunnar Mills $scope.devices[index].isActive = false; 41*418db63cSGunnar Mills var server = $scope.devices[index].nbdServer; 42*418db63cSGunnar Mills server.stop(); 43*418db63cSGunnar Mills }; 44*418db63cSGunnar Mills 45*418db63cSGunnar Mills $scope.resetFile = function(index) { 46*418db63cSGunnar Mills document.getElementById('file-upload').value = ''; 47*418db63cSGunnar Mills $scope.devices[index].file = ''; 48*418db63cSGunnar Mills }; 49*418db63cSGunnar Mills 50*418db63cSGunnar Mills function findExistingConnection(vmDevice) { 51*418db63cSGunnar Mills // Checks with existing connections kept in nbdServerService for an open 52*418db63cSGunnar Mills // Websocket connection. 53*418db63cSGunnar Mills var existingConnectionsMap = nbdServerService.getExistingConnections(); 54*418db63cSGunnar Mills if (existingConnectionsMap.hasOwnProperty(vmDevice.id)) { 55*418db63cSGunnar Mills // Open ws will have a ready state of 1 56*418db63cSGunnar Mills if (existingConnectionsMap[vmDevice.id].server.ws.readyState === 1) { 57*418db63cSGunnar Mills vmDevice.isActive = true; 58*418db63cSGunnar Mills vmDevice.file = existingConnectionsMap[vmDevice.id].file; 59*418db63cSGunnar Mills vmDevice.nbdServer = existingConnectionsMap[vmDevice.id].server; 60*418db63cSGunnar Mills } 61*418db63cSGunnar Mills } 62*418db63cSGunnar Mills return vmDevice; 63*418db63cSGunnar Mills } 64*418db63cSGunnar Mills } 65*418db63cSGunnar Mills ]); 66*418db63cSGunnar Mills})(angular); 67*418db63cSGunnar Mills 68*418db63cSGunnar Mills/* handshake flags */ 69*418db63cSGunnar Millsconst NBD_FLAG_FIXED_NEWSTYLE = 0x1; 70*418db63cSGunnar Millsconst NBD_FLAG_NO_ZEROES = 0x2; 71*418db63cSGunnar Mills 72*418db63cSGunnar Mills/* transmission flags */ 73*418db63cSGunnar Millsconst NBD_FLAG_HAS_FLAGS = 0x1; 74*418db63cSGunnar Millsconst NBD_FLAG_READ_ONLY = 0x2; 75*418db63cSGunnar Mills 76*418db63cSGunnar Mills/* option negotiation */ 77*418db63cSGunnar Millsconst NBD_OPT_EXPORT_NAME = 0x1; 78*418db63cSGunnar Millsconst NBD_REP_FLAG_ERROR = 0x1 << 31; 79*418db63cSGunnar Millsconst NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1; 80*418db63cSGunnar Mills 81*418db63cSGunnar Mills/* command definitions */ 82*418db63cSGunnar Millsconst NBD_CMD_READ = 0; 83*418db63cSGunnar Millsconst NBD_CMD_WRITE = 1; 84*418db63cSGunnar Millsconst NBD_CMD_DISC = 2; 85*418db63cSGunnar Millsconst NBD_CMD_TRIM = 4; 86*418db63cSGunnar Mills 87*418db63cSGunnar Mills/* errno */ 88*418db63cSGunnar Millsconst EPERM = 1; 89*418db63cSGunnar Millsconst EIO = 5; 90*418db63cSGunnar Millsconst EINVAL = 22; 91*418db63cSGunnar Millsconst ENOSPC = 28; 92*418db63cSGunnar Mills 93*418db63cSGunnar Mills/* internal object state */ 94*418db63cSGunnar Millsconst NBD_STATE_UNKNOWN = 1; 95*418db63cSGunnar Millsconst NBD_STATE_OPEN = 2; 96*418db63cSGunnar Millsconst NBD_STATE_WAIT_CFLAGS = 3; 97*418db63cSGunnar Millsconst NBD_STATE_WAIT_OPTION = 4; 98*418db63cSGunnar Millsconst NBD_STATE_TRANSMISSION = 5; 99*418db63cSGunnar Mills 100*418db63cSGunnar Millsfunction NBDServer(endpoint, file, id) { 101*418db63cSGunnar Mills this.file = file; 102*418db63cSGunnar Mills this.id = id; 103*418db63cSGunnar Mills this.endpoint = endpoint; 104*418db63cSGunnar Mills this.ws = null; 105*418db63cSGunnar Mills this.state = NBD_STATE_UNKNOWN; 106*418db63cSGunnar Mills this.msgbuf = null; 107*418db63cSGunnar Mills 108*418db63cSGunnar Mills this.start = function() { 109*418db63cSGunnar Mills this.ws = new WebSocket(this.endpoint); 110*418db63cSGunnar Mills this.state = NBD_STATE_OPEN; 111*418db63cSGunnar Mills this.ws.binaryType = 'arraybuffer'; 112*418db63cSGunnar Mills this.ws.onmessage = this._on_ws_message.bind(this); 113*418db63cSGunnar Mills this.ws.onopen = this._on_ws_open.bind(this); 114*418db63cSGunnar Mills this.ws.onclose = this._on_ws_close.bind(this); 115*418db63cSGunnar Mills this.ws.onerror = this._on_ws_error.bind(this); 116*418db63cSGunnar Mills }; 117*418db63cSGunnar Mills 118*418db63cSGunnar Mills this.stop = function() { 119*418db63cSGunnar Mills this.ws.close(); 120*418db63cSGunnar Mills this.state = NBD_STATE_UNKNOWN; 121*418db63cSGunnar Mills }; 122*418db63cSGunnar Mills 123*418db63cSGunnar Mills this._on_ws_error = function(ev) { 124*418db63cSGunnar Mills console.log('vm/0/' + id + 'error: ' + ev); 125*418db63cSGunnar Mills }; 126*418db63cSGunnar Mills 127*418db63cSGunnar Mills this._on_ws_close = function(ev) { 128*418db63cSGunnar Mills console.log( 129*418db63cSGunnar Mills 'vm/0/' + id + ' closed with code: ' + ev.code + 130*418db63cSGunnar Mills ' reason: ' + ev.reason); 131*418db63cSGunnar Mills }; 132*418db63cSGunnar Mills 133*418db63cSGunnar Mills /* websocket event handlers */ 134*418db63cSGunnar Mills this._on_ws_open = function(ev) { 135*418db63cSGunnar Mills console.log('vm/0/' + id + ' opened'); 136*418db63cSGunnar Mills this.client = { 137*418db63cSGunnar Mills flags: 0, 138*418db63cSGunnar Mills }; 139*418db63cSGunnar Mills this._negotiate(); 140*418db63cSGunnar Mills }; 141*418db63cSGunnar Mills 142*418db63cSGunnar Mills this._on_ws_message = function(ev) { 143*418db63cSGunnar Mills var data = ev.data; 144*418db63cSGunnar Mills 145*418db63cSGunnar Mills if (this.msgbuf == null) { 146*418db63cSGunnar Mills this.msgbuf = data; 147*418db63cSGunnar Mills } else { 148*418db63cSGunnar Mills var tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength); 149*418db63cSGunnar Mills tmp.set(new Uint8Array(this.msgbuf), 0); 150*418db63cSGunnar Mills tmp.set(new Uint8Array(data), this.msgbuf.byteLength); 151*418db63cSGunnar Mills this.msgbuf = tmp.buffer; 152*418db63cSGunnar Mills } 153*418db63cSGunnar Mills 154*418db63cSGunnar Mills for (;;) { 155*418db63cSGunnar Mills var handler = this.recv_handlers[this.state]; 156*418db63cSGunnar Mills if (!handler) { 157*418db63cSGunnar Mills console.log('no handler for state ' + this.state); 158*418db63cSGunnar Mills this.stop(); 159*418db63cSGunnar Mills break; 160*418db63cSGunnar Mills } 161*418db63cSGunnar Mills 162*418db63cSGunnar Mills var consumed = handler(this.msgbuf); 163*418db63cSGunnar Mills if (consumed < 0) { 164*418db63cSGunnar Mills console.log( 165*418db63cSGunnar Mills 'handler[state=' + this.state + '] returned error ' + consumed); 166*418db63cSGunnar Mills this.stop(); 167*418db63cSGunnar Mills break; 168*418db63cSGunnar Mills } 169*418db63cSGunnar Mills 170*418db63cSGunnar Mills if (consumed == 0) { 171*418db63cSGunnar Mills break; 172*418db63cSGunnar Mills } 173*418db63cSGunnar Mills 174*418db63cSGunnar Mills if (consumed > 0) { 175*418db63cSGunnar Mills if (consumed == this.msgbuf.byteLength) { 176*418db63cSGunnar Mills this.msgbuf = null; 177*418db63cSGunnar Mills break; 178*418db63cSGunnar Mills } 179*418db63cSGunnar Mills this.msgbuf = this.msgbuf.slice(consumed); 180*418db63cSGunnar Mills } 181*418db63cSGunnar Mills } 182*418db63cSGunnar Mills }; 183*418db63cSGunnar Mills 184*418db63cSGunnar Mills this._negotiate = function() { 185*418db63cSGunnar Mills var buf = new ArrayBuffer(18); 186*418db63cSGunnar Mills var data = new DataView(buf, 0, 18); 187*418db63cSGunnar Mills 188*418db63cSGunnar Mills /* NBD magic: NBDMAGIC */ 189*418db63cSGunnar Mills data.setUint32(0, 0x4e42444d); 190*418db63cSGunnar Mills data.setUint32(4, 0x41474943); 191*418db63cSGunnar Mills 192*418db63cSGunnar Mills /* newstyle negotiation: IHAVEOPT */ 193*418db63cSGunnar Mills data.setUint32(8, 0x49484156); 194*418db63cSGunnar Mills data.setUint32(12, 0x454F5054); 195*418db63cSGunnar Mills 196*418db63cSGunnar Mills /* flags: fixed newstyle negotiation, no padding */ 197*418db63cSGunnar Mills data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES); 198*418db63cSGunnar Mills 199*418db63cSGunnar Mills this.state = NBD_STATE_WAIT_CFLAGS; 200*418db63cSGunnar Mills this.ws.send(buf); 201*418db63cSGunnar Mills }; 202*418db63cSGunnar Mills 203*418db63cSGunnar Mills /* handlers */ 204*418db63cSGunnar Mills this._handle_cflags = function(buf) { 205*418db63cSGunnar Mills if (buf.byteLength < 4) { 206*418db63cSGunnar Mills return 0; 207*418db63cSGunnar Mills } 208*418db63cSGunnar Mills 209*418db63cSGunnar Mills var data = new DataView(buf, 0, 4); 210*418db63cSGunnar Mills this.client.flags = data.getUint32(0); 211*418db63cSGunnar Mills 212*418db63cSGunnar Mills this.state = NBD_STATE_WAIT_OPTION; 213*418db63cSGunnar Mills return 4; 214*418db63cSGunnar Mills }; 215*418db63cSGunnar Mills 216*418db63cSGunnar Mills this._handle_option = function(buf) { 217*418db63cSGunnar Mills if (buf.byteLength < 16) return 0; 218*418db63cSGunnar Mills 219*418db63cSGunnar Mills var data = new DataView(buf, 0, 16); 220*418db63cSGunnar Mills if (data.getUint32(0) != 0x49484156 || data.getUint32(4) != 0x454F5054) { 221*418db63cSGunnar Mills console.log('invalid option magic'); 222*418db63cSGunnar Mills return -1; 223*418db63cSGunnar Mills } 224*418db63cSGunnar Mills 225*418db63cSGunnar Mills var opt = data.getUint32(8); 226*418db63cSGunnar Mills var len = data.getUint32(12); 227*418db63cSGunnar Mills 228*418db63cSGunnar Mills 229*418db63cSGunnar Mills if (buf.byteLength < 16 + len) { 230*418db63cSGunnar Mills return 0; 231*418db63cSGunnar Mills } 232*418db63cSGunnar Mills 233*418db63cSGunnar Mills switch (opt) { 234*418db63cSGunnar Mills case NBD_OPT_EXPORT_NAME: 235*418db63cSGunnar Mills var n = 10; 236*418db63cSGunnar Mills if (!(this.client.flags & NBD_FLAG_NO_ZEROES)) n += 124; 237*418db63cSGunnar Mills var resp = new ArrayBuffer(n); 238*418db63cSGunnar Mills var view = new DataView(resp, 0, 10); 239*418db63cSGunnar Mills /* export size. */ 240*418db63cSGunnar Mills var size = this.file.size; 241*418db63cSGunnar Mills view.setUint32(0, Math.floor(size / (2 ** 32))); 242*418db63cSGunnar Mills view.setUint32(4, size & 0xffffffff); 243*418db63cSGunnar Mills /* transmission flags: read-only */ 244*418db63cSGunnar Mills view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY); 245*418db63cSGunnar Mills this.ws.send(resp); 246*418db63cSGunnar Mills 247*418db63cSGunnar Mills this.state = NBD_STATE_TRANSMISSION; 248*418db63cSGunnar Mills break; 249*418db63cSGunnar Mills 250*418db63cSGunnar Mills default: 251*418db63cSGunnar Mills console.log('handle_option: Unsupported option: ' + opt); 252*418db63cSGunnar Mills /* reject other options */ 253*418db63cSGunnar Mills var resp = new ArrayBuffer(20); 254*418db63cSGunnar Mills var view = new DataView(resp, 0, 20); 255*418db63cSGunnar Mills view.setUint32(0, 0x0003e889); 256*418db63cSGunnar Mills view.setUint32(4, 0x045565a9); 257*418db63cSGunnar Mills view.setUint32(8, opt); 258*418db63cSGunnar Mills view.setUint32(12, NBD_REP_ERR_UNSUP); 259*418db63cSGunnar Mills view.setUint32(16, 0); 260*418db63cSGunnar Mills this.ws.send(resp); 261*418db63cSGunnar Mills } 262*418db63cSGunnar Mills 263*418db63cSGunnar Mills return 16 + len; 264*418db63cSGunnar Mills }; 265*418db63cSGunnar Mills 266*418db63cSGunnar Mills this._create_cmd_response = function(req, rc, data = null) { 267*418db63cSGunnar Mills var len = 16; 268*418db63cSGunnar Mills if (data) len += data.byteLength; 269*418db63cSGunnar Mills var resp = new ArrayBuffer(len); 270*418db63cSGunnar Mills var view = new DataView(resp, 0, 16); 271*418db63cSGunnar Mills view.setUint32(0, 0x67446698); 272*418db63cSGunnar Mills view.setUint32(4, rc); 273*418db63cSGunnar Mills view.setUint32(8, req.handle_msB); 274*418db63cSGunnar Mills view.setUint32(12, req.handle_lsB); 275*418db63cSGunnar Mills if (data) new Uint8Array(resp, 16).set(new Uint8Array(data)); 276*418db63cSGunnar Mills return resp; 277*418db63cSGunnar Mills }; 278*418db63cSGunnar Mills 279*418db63cSGunnar Mills this._handle_cmd = function(buf) { 280*418db63cSGunnar Mills if (buf.byteLength < 28) { 281*418db63cSGunnar Mills return 0; 282*418db63cSGunnar Mills } 283*418db63cSGunnar Mills 284*418db63cSGunnar Mills var view = new DataView(buf, 0, 28); 285*418db63cSGunnar Mills 286*418db63cSGunnar Mills if (view.getUint32(0) != 0x25609513) { 287*418db63cSGunnar Mills console.log('invalid request magic'); 288*418db63cSGunnar Mills return -1; 289*418db63cSGunnar Mills } 290*418db63cSGunnar Mills 291*418db63cSGunnar Mills var req = { 292*418db63cSGunnar Mills flags: view.getUint16(4), 293*418db63cSGunnar Mills type: view.getUint16(6), 294*418db63cSGunnar Mills handle_msB: view.getUint32(8), 295*418db63cSGunnar Mills handle_lsB: view.getUint32(12), 296*418db63cSGunnar Mills offset_msB: view.getUint32(16), 297*418db63cSGunnar Mills offset_lsB: view.getUint32(20), 298*418db63cSGunnar Mills length: view.getUint32(24), 299*418db63cSGunnar Mills }; 300*418db63cSGunnar Mills 301*418db63cSGunnar Mills /* we don't support writes, so nothing needs the data at present */ 302*418db63cSGunnar Mills /* req.data = buf.slice(28); */ 303*418db63cSGunnar Mills 304*418db63cSGunnar Mills var err = 0; 305*418db63cSGunnar Mills var consumed = 28; 306*418db63cSGunnar Mills 307*418db63cSGunnar Mills /* the command handlers return 0 on success, and send their 308*418db63cSGunnar Mills * own response. Otherwise, a non-zero error code will be 309*418db63cSGunnar Mills * used as a simple error response 310*418db63cSGunnar Mills */ 311*418db63cSGunnar Mills switch (req.type) { 312*418db63cSGunnar Mills case NBD_CMD_READ: 313*418db63cSGunnar Mills err = this._handle_cmd_read(req); 314*418db63cSGunnar Mills break; 315*418db63cSGunnar Mills 316*418db63cSGunnar Mills case NBD_CMD_DISC: 317*418db63cSGunnar Mills err = this._handle_cmd_disconnect(req); 318*418db63cSGunnar Mills break; 319*418db63cSGunnar Mills 320*418db63cSGunnar Mills case NBD_CMD_WRITE: 321*418db63cSGunnar Mills /* we also need length bytes of data to consume a write 322*418db63cSGunnar Mills * request */ 323*418db63cSGunnar Mills if (buf.byteLength < 28 + req.length) { 324*418db63cSGunnar Mills return 0; 325*418db63cSGunnar Mills } 326*418db63cSGunnar Mills consumed += req.length; 327*418db63cSGunnar Mills err = EPERM; 328*418db63cSGunnar Mills break; 329*418db63cSGunnar Mills 330*418db63cSGunnar Mills case NBD_CMD_TRIM: 331*418db63cSGunnar Mills err = EPERM; 332*418db63cSGunnar Mills break; 333*418db63cSGunnar Mills 334*418db63cSGunnar Mills default: 335*418db63cSGunnar Mills console.log('invalid command 0x' + req.type.toString(16)); 336*418db63cSGunnar Mills err = EINVAL; 337*418db63cSGunnar Mills } 338*418db63cSGunnar Mills 339*418db63cSGunnar Mills if (err) { 340*418db63cSGunnar Mills console.log('error handle_cmd: ' + err); 341*418db63cSGunnar Mills var resp = this._create_cmd_response(req, err); 342*418db63cSGunnar Mills this.ws.send(resp); 343*418db63cSGunnar Mills } 344*418db63cSGunnar Mills 345*418db63cSGunnar Mills return consumed; 346*418db63cSGunnar Mills }; 347*418db63cSGunnar Mills 348*418db63cSGunnar Mills this._handle_cmd_read = function(req) { 349*418db63cSGunnar Mills var offset; 350*418db63cSGunnar Mills 351*418db63cSGunnar Mills offset = (req.offset_msB * 2 ** 32) + req.offset_lsB; 352*418db63cSGunnar Mills 353*418db63cSGunnar Mills if (offset > Number.MAX_SAFE_INTEGER) return ENOSPC; 354*418db63cSGunnar Mills 355*418db63cSGunnar Mills if (offset + req.length > Number.MAX_SAFE_INTEGER) return ENOSPC; 356*418db63cSGunnar Mills 357*418db63cSGunnar Mills if (offset + req.length > file.size) return ENOSPC; 358*418db63cSGunnar Mills 359*418db63cSGunnar Mills var blob = this.file.slice(offset, offset + req.length); 360*418db63cSGunnar Mills var reader = new FileReader(); 361*418db63cSGunnar Mills 362*418db63cSGunnar Mills reader.onload = (function(ev) { 363*418db63cSGunnar Mills var reader = ev.target; 364*418db63cSGunnar Mills if (reader.readyState != FileReader.DONE) return; 365*418db63cSGunnar Mills var resp = 366*418db63cSGunnar Mills this._create_cmd_response(req, 0, reader.result); 367*418db63cSGunnar Mills this.ws.send(resp); 368*418db63cSGunnar Mills }).bind(this); 369*418db63cSGunnar Mills 370*418db63cSGunnar Mills reader.onerror = (function(ev) { 371*418db63cSGunnar Mills var reader = ev.target; 372*418db63cSGunnar Mills console.log('error reading file: ' + reader.error); 373*418db63cSGunnar Mills var resp = this._create_cmd_response(req, EIO); 374*418db63cSGunnar Mills this.ws.send(resp); 375*418db63cSGunnar Mills }).bind(this); 376*418db63cSGunnar Mills reader.readAsArrayBuffer(blob); 377*418db63cSGunnar Mills 378*418db63cSGunnar Mills return 0; 379*418db63cSGunnar Mills }; 380*418db63cSGunnar Mills 381*418db63cSGunnar Mills this._handle_cmd_disconnect = function(req) { 382*418db63cSGunnar Mills this.stop(); 383*418db63cSGunnar Mills return 0; 384*418db63cSGunnar Mills }; 385*418db63cSGunnar Mills 386*418db63cSGunnar Mills this.recv_handlers = Object.freeze({ 387*418db63cSGunnar Mills [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this), 388*418db63cSGunnar Mills [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this), 389*418db63cSGunnar Mills [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this), 390*418db63cSGunnar Mills }); 391*418db63cSGunnar Mills} 392