1418db63cSGunnar Mills/** 2418db63cSGunnar Mills * Controller for virtual-media 3418db63cSGunnar Mills * 4418db63cSGunnar Mills * @module app/serverControl 5418db63cSGunnar Mills * @exports virtualMediaController 6418db63cSGunnar Mills * @name virtualMediaController 7418db63cSGunnar Mills */ 8418db63cSGunnar Mills 9418db63cSGunnar Millswindow.angular && (function(angular) { 10418db63cSGunnar Mills 'use strict'; 11418db63cSGunnar Mills 12418db63cSGunnar Mills angular.module('app.serverControl').controller('virtualMediaController', [ 13*4a16a026SJames Feist '$scope', '$cookies', 'APIUtils', 'toastService', 'dataService', 14*4a16a026SJames Feist 'nbdServerService', 15*4a16a026SJames Feist function( 16*4a16a026SJames Feist $scope, $cookies, APIUtils, toastService, dataService, 17*4a16a026SJames Feist nbdServerService) { 18418db63cSGunnar Mills $scope.devices = []; 19418db63cSGunnar Mills 20418db63cSGunnar Mills // Only one Virtual Media WebSocket device is currently available. 21418db63cSGunnar Mills // Path is /vm/0/0. 22418db63cSGunnar Mills // TODO: Support more than 1 VM device, when backend support is added. 23418db63cSGunnar Mills var vmDevice = {}; 24418db63cSGunnar Mills // Hardcode to 0 since /vm/0/0. Last 0 is the device ID. 25418db63cSGunnar Mills // To support more than 1 device ID, replace with a call to get the 26418db63cSGunnar Mills // device IDs and names. 27418db63cSGunnar Mills vmDevice.id = 0; 28418db63cSGunnar Mills vmDevice.deviceName = 'Virtual media device'; 29418db63cSGunnar Mills findExistingConnection(vmDevice); 30418db63cSGunnar Mills $scope.devices.push(vmDevice); 31418db63cSGunnar Mills 32418db63cSGunnar Mills $scope.startVM = function(index) { 33418db63cSGunnar Mills $scope.devices[index].isActive = true; 34418db63cSGunnar Mills var file = $scope.devices[index].file; 35418db63cSGunnar Mills var id = $scope.devices[index].id; 36418db63cSGunnar Mills var host = dataService.getHost().replace('https://', ''); 37*4a16a026SJames Feist var token = $cookies.get('XSRF-TOKEN'); 38*4a16a026SJames Feist var server = 39*4a16a026SJames Feist new NBDServer('wss://' + host + '/vm/0/' + id, token, file, id); 40418db63cSGunnar Mills $scope.devices[index].nbdServer = server; 41418db63cSGunnar Mills nbdServerService.addConnection(id, server, file); 42418db63cSGunnar Mills server.start(); 43418db63cSGunnar Mills }; 44418db63cSGunnar Mills $scope.stopVM = function(index) { 45418db63cSGunnar Mills $scope.devices[index].isActive = false; 46418db63cSGunnar Mills var server = $scope.devices[index].nbdServer; 47418db63cSGunnar Mills server.stop(); 48418db63cSGunnar Mills }; 49418db63cSGunnar Mills 50418db63cSGunnar Mills $scope.resetFile = function(index) { 51418db63cSGunnar Mills document.getElementById('file-upload').value = ''; 52418db63cSGunnar Mills $scope.devices[index].file = ''; 53418db63cSGunnar Mills }; 54418db63cSGunnar Mills 55418db63cSGunnar Mills function findExistingConnection(vmDevice) { 56418db63cSGunnar Mills // Checks with existing connections kept in nbdServerService for an open 57418db63cSGunnar Mills // Websocket connection. 58418db63cSGunnar Mills var existingConnectionsMap = nbdServerService.getExistingConnections(); 59418db63cSGunnar Mills if (existingConnectionsMap.hasOwnProperty(vmDevice.id)) { 60418db63cSGunnar Mills // Open ws will have a ready state of 1 61418db63cSGunnar Mills if (existingConnectionsMap[vmDevice.id].server.ws.readyState === 1) { 62418db63cSGunnar Mills vmDevice.isActive = true; 63418db63cSGunnar Mills vmDevice.file = existingConnectionsMap[vmDevice.id].file; 64418db63cSGunnar Mills vmDevice.nbdServer = existingConnectionsMap[vmDevice.id].server; 65418db63cSGunnar Mills } 66418db63cSGunnar Mills } 67418db63cSGunnar Mills return vmDevice; 68418db63cSGunnar Mills } 69418db63cSGunnar Mills } 70418db63cSGunnar Mills ]); 71418db63cSGunnar Mills})(angular); 72418db63cSGunnar Mills 73418db63cSGunnar Mills/* handshake flags */ 74418db63cSGunnar Millsconst NBD_FLAG_FIXED_NEWSTYLE = 0x1; 75418db63cSGunnar Millsconst NBD_FLAG_NO_ZEROES = 0x2; 76418db63cSGunnar Mills 77418db63cSGunnar Mills/* transmission flags */ 78418db63cSGunnar Millsconst NBD_FLAG_HAS_FLAGS = 0x1; 79418db63cSGunnar Millsconst NBD_FLAG_READ_ONLY = 0x2; 80418db63cSGunnar Mills 81418db63cSGunnar Mills/* option negotiation */ 82418db63cSGunnar Millsconst NBD_OPT_EXPORT_NAME = 0x1; 83418db63cSGunnar Millsconst NBD_REP_FLAG_ERROR = 0x1 << 31; 84418db63cSGunnar Millsconst NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1; 85418db63cSGunnar Mills 86418db63cSGunnar Mills/* command definitions */ 87418db63cSGunnar Millsconst NBD_CMD_READ = 0; 88418db63cSGunnar Millsconst NBD_CMD_WRITE = 1; 89418db63cSGunnar Millsconst NBD_CMD_DISC = 2; 90418db63cSGunnar Millsconst NBD_CMD_TRIM = 4; 91418db63cSGunnar Mills 92418db63cSGunnar Mills/* errno */ 93418db63cSGunnar Millsconst EPERM = 1; 94418db63cSGunnar Millsconst EIO = 5; 95418db63cSGunnar Millsconst EINVAL = 22; 96418db63cSGunnar Millsconst ENOSPC = 28; 97418db63cSGunnar Mills 98418db63cSGunnar Mills/* internal object state */ 99418db63cSGunnar Millsconst NBD_STATE_UNKNOWN = 1; 100418db63cSGunnar Millsconst NBD_STATE_OPEN = 2; 101418db63cSGunnar Millsconst NBD_STATE_WAIT_CFLAGS = 3; 102418db63cSGunnar Millsconst NBD_STATE_WAIT_OPTION = 4; 103418db63cSGunnar Millsconst NBD_STATE_TRANSMISSION = 5; 104418db63cSGunnar Mills 105*4a16a026SJames Feistfunction NBDServer(endpoint, token, file, id) { 106418db63cSGunnar Mills this.file = file; 107418db63cSGunnar Mills this.id = id; 108418db63cSGunnar Mills this.endpoint = endpoint; 109418db63cSGunnar Mills this.ws = null; 110418db63cSGunnar Mills this.state = NBD_STATE_UNKNOWN; 111418db63cSGunnar Mills this.msgbuf = null; 112418db63cSGunnar Mills 113418db63cSGunnar Mills this.start = function() { 114*4a16a026SJames Feist this.ws = new WebSocket(this.endpoint, [token]); 115418db63cSGunnar Mills this.state = NBD_STATE_OPEN; 116418db63cSGunnar Mills this.ws.binaryType = 'arraybuffer'; 117418db63cSGunnar Mills this.ws.onmessage = this._on_ws_message.bind(this); 118418db63cSGunnar Mills this.ws.onopen = this._on_ws_open.bind(this); 119418db63cSGunnar Mills this.ws.onclose = this._on_ws_close.bind(this); 120418db63cSGunnar Mills this.ws.onerror = this._on_ws_error.bind(this); 121418db63cSGunnar Mills }; 122418db63cSGunnar Mills 123418db63cSGunnar Mills this.stop = function() { 124418db63cSGunnar Mills this.ws.close(); 125418db63cSGunnar Mills this.state = NBD_STATE_UNKNOWN; 126418db63cSGunnar Mills }; 127418db63cSGunnar Mills 128418db63cSGunnar Mills this._on_ws_error = function(ev) { 129418db63cSGunnar Mills console.log('vm/0/' + id + 'error: ' + ev); 130418db63cSGunnar Mills }; 131418db63cSGunnar Mills 132418db63cSGunnar Mills this._on_ws_close = function(ev) { 133418db63cSGunnar Mills console.log( 134418db63cSGunnar Mills 'vm/0/' + id + ' closed with code: ' + ev.code + 135418db63cSGunnar Mills ' reason: ' + ev.reason); 136418db63cSGunnar Mills }; 137418db63cSGunnar Mills 138418db63cSGunnar Mills /* websocket event handlers */ 139418db63cSGunnar Mills this._on_ws_open = function(ev) { 140418db63cSGunnar Mills console.log('vm/0/' + id + ' opened'); 141418db63cSGunnar Mills this.client = { 142418db63cSGunnar Mills flags: 0, 143418db63cSGunnar Mills }; 144418db63cSGunnar Mills this._negotiate(); 145418db63cSGunnar Mills }; 146418db63cSGunnar Mills 147418db63cSGunnar Mills this._on_ws_message = function(ev) { 148418db63cSGunnar Mills var data = ev.data; 149418db63cSGunnar Mills 150418db63cSGunnar Mills if (this.msgbuf == null) { 151418db63cSGunnar Mills this.msgbuf = data; 152418db63cSGunnar Mills } else { 153418db63cSGunnar Mills var tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength); 154418db63cSGunnar Mills tmp.set(new Uint8Array(this.msgbuf), 0); 155418db63cSGunnar Mills tmp.set(new Uint8Array(data), this.msgbuf.byteLength); 156418db63cSGunnar Mills this.msgbuf = tmp.buffer; 157418db63cSGunnar Mills } 158418db63cSGunnar Mills 159418db63cSGunnar Mills for (;;) { 160418db63cSGunnar Mills var handler = this.recv_handlers[this.state]; 161418db63cSGunnar Mills if (!handler) { 162418db63cSGunnar Mills console.log('no handler for state ' + this.state); 163418db63cSGunnar Mills this.stop(); 164418db63cSGunnar Mills break; 165418db63cSGunnar Mills } 166418db63cSGunnar Mills 167418db63cSGunnar Mills var consumed = handler(this.msgbuf); 168418db63cSGunnar Mills if (consumed < 0) { 169418db63cSGunnar Mills console.log( 170418db63cSGunnar Mills 'handler[state=' + this.state + '] returned error ' + consumed); 171418db63cSGunnar Mills this.stop(); 172418db63cSGunnar Mills break; 173418db63cSGunnar Mills } 174418db63cSGunnar Mills 175418db63cSGunnar Mills if (consumed == 0) { 176418db63cSGunnar Mills break; 177418db63cSGunnar Mills } 178418db63cSGunnar Mills 179418db63cSGunnar Mills if (consumed > 0) { 180418db63cSGunnar Mills if (consumed == this.msgbuf.byteLength) { 181418db63cSGunnar Mills this.msgbuf = null; 182418db63cSGunnar Mills break; 183418db63cSGunnar Mills } 184418db63cSGunnar Mills this.msgbuf = this.msgbuf.slice(consumed); 185418db63cSGunnar Mills } 186418db63cSGunnar Mills } 187418db63cSGunnar Mills }; 188418db63cSGunnar Mills 189418db63cSGunnar Mills this._negotiate = function() { 190418db63cSGunnar Mills var buf = new ArrayBuffer(18); 191418db63cSGunnar Mills var data = new DataView(buf, 0, 18); 192418db63cSGunnar Mills 193418db63cSGunnar Mills /* NBD magic: NBDMAGIC */ 194418db63cSGunnar Mills data.setUint32(0, 0x4e42444d); 195418db63cSGunnar Mills data.setUint32(4, 0x41474943); 196418db63cSGunnar Mills 197418db63cSGunnar Mills /* newstyle negotiation: IHAVEOPT */ 198418db63cSGunnar Mills data.setUint32(8, 0x49484156); 199418db63cSGunnar Mills data.setUint32(12, 0x454F5054); 200418db63cSGunnar Mills 201418db63cSGunnar Mills /* flags: fixed newstyle negotiation, no padding */ 202418db63cSGunnar Mills data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES); 203418db63cSGunnar Mills 204418db63cSGunnar Mills this.state = NBD_STATE_WAIT_CFLAGS; 205418db63cSGunnar Mills this.ws.send(buf); 206418db63cSGunnar Mills }; 207418db63cSGunnar Mills 208418db63cSGunnar Mills /* handlers */ 209418db63cSGunnar Mills this._handle_cflags = function(buf) { 210418db63cSGunnar Mills if (buf.byteLength < 4) { 211418db63cSGunnar Mills return 0; 212418db63cSGunnar Mills } 213418db63cSGunnar Mills 214418db63cSGunnar Mills var data = new DataView(buf, 0, 4); 215418db63cSGunnar Mills this.client.flags = data.getUint32(0); 216418db63cSGunnar Mills 217418db63cSGunnar Mills this.state = NBD_STATE_WAIT_OPTION; 218418db63cSGunnar Mills return 4; 219418db63cSGunnar Mills }; 220418db63cSGunnar Mills 221418db63cSGunnar Mills this._handle_option = function(buf) { 222418db63cSGunnar Mills if (buf.byteLength < 16) return 0; 223418db63cSGunnar Mills 224418db63cSGunnar Mills var data = new DataView(buf, 0, 16); 225418db63cSGunnar Mills if (data.getUint32(0) != 0x49484156 || data.getUint32(4) != 0x454F5054) { 226418db63cSGunnar Mills console.log('invalid option magic'); 227418db63cSGunnar Mills return -1; 228418db63cSGunnar Mills } 229418db63cSGunnar Mills 230418db63cSGunnar Mills var opt = data.getUint32(8); 231418db63cSGunnar Mills var len = data.getUint32(12); 232418db63cSGunnar Mills 233418db63cSGunnar Mills 234418db63cSGunnar Mills if (buf.byteLength < 16 + len) { 235418db63cSGunnar Mills return 0; 236418db63cSGunnar Mills } 237418db63cSGunnar Mills 238418db63cSGunnar Mills switch (opt) { 239418db63cSGunnar Mills case NBD_OPT_EXPORT_NAME: 240418db63cSGunnar Mills var n = 10; 241418db63cSGunnar Mills if (!(this.client.flags & NBD_FLAG_NO_ZEROES)) n += 124; 242418db63cSGunnar Mills var resp = new ArrayBuffer(n); 243418db63cSGunnar Mills var view = new DataView(resp, 0, 10); 244418db63cSGunnar Mills /* export size. */ 245418db63cSGunnar Mills var size = this.file.size; 246418db63cSGunnar Mills view.setUint32(0, Math.floor(size / (2 ** 32))); 247418db63cSGunnar Mills view.setUint32(4, size & 0xffffffff); 248418db63cSGunnar Mills /* transmission flags: read-only */ 249418db63cSGunnar Mills view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY); 250418db63cSGunnar Mills this.ws.send(resp); 251418db63cSGunnar Mills 252418db63cSGunnar Mills this.state = NBD_STATE_TRANSMISSION; 253418db63cSGunnar Mills break; 254418db63cSGunnar Mills 255418db63cSGunnar Mills default: 256418db63cSGunnar Mills console.log('handle_option: Unsupported option: ' + opt); 257418db63cSGunnar Mills /* reject other options */ 258418db63cSGunnar Mills var resp = new ArrayBuffer(20); 259418db63cSGunnar Mills var view = new DataView(resp, 0, 20); 260418db63cSGunnar Mills view.setUint32(0, 0x0003e889); 261418db63cSGunnar Mills view.setUint32(4, 0x045565a9); 262418db63cSGunnar Mills view.setUint32(8, opt); 263418db63cSGunnar Mills view.setUint32(12, NBD_REP_ERR_UNSUP); 264418db63cSGunnar Mills view.setUint32(16, 0); 265418db63cSGunnar Mills this.ws.send(resp); 266418db63cSGunnar Mills } 267418db63cSGunnar Mills 268418db63cSGunnar Mills return 16 + len; 269418db63cSGunnar Mills }; 270418db63cSGunnar Mills 271418db63cSGunnar Mills this._create_cmd_response = function(req, rc, data = null) { 272418db63cSGunnar Mills var len = 16; 273418db63cSGunnar Mills if (data) len += data.byteLength; 274418db63cSGunnar Mills var resp = new ArrayBuffer(len); 275418db63cSGunnar Mills var view = new DataView(resp, 0, 16); 276418db63cSGunnar Mills view.setUint32(0, 0x67446698); 277418db63cSGunnar Mills view.setUint32(4, rc); 278418db63cSGunnar Mills view.setUint32(8, req.handle_msB); 279418db63cSGunnar Mills view.setUint32(12, req.handle_lsB); 280418db63cSGunnar Mills if (data) new Uint8Array(resp, 16).set(new Uint8Array(data)); 281418db63cSGunnar Mills return resp; 282418db63cSGunnar Mills }; 283418db63cSGunnar Mills 284418db63cSGunnar Mills this._handle_cmd = function(buf) { 285418db63cSGunnar Mills if (buf.byteLength < 28) { 286418db63cSGunnar Mills return 0; 287418db63cSGunnar Mills } 288418db63cSGunnar Mills 289418db63cSGunnar Mills var view = new DataView(buf, 0, 28); 290418db63cSGunnar Mills 291418db63cSGunnar Mills if (view.getUint32(0) != 0x25609513) { 292418db63cSGunnar Mills console.log('invalid request magic'); 293418db63cSGunnar Mills return -1; 294418db63cSGunnar Mills } 295418db63cSGunnar Mills 296418db63cSGunnar Mills var req = { 297418db63cSGunnar Mills flags: view.getUint16(4), 298418db63cSGunnar Mills type: view.getUint16(6), 299418db63cSGunnar Mills handle_msB: view.getUint32(8), 300418db63cSGunnar Mills handle_lsB: view.getUint32(12), 301418db63cSGunnar Mills offset_msB: view.getUint32(16), 302418db63cSGunnar Mills offset_lsB: view.getUint32(20), 303418db63cSGunnar Mills length: view.getUint32(24), 304418db63cSGunnar Mills }; 305418db63cSGunnar Mills 306418db63cSGunnar Mills /* we don't support writes, so nothing needs the data at present */ 307418db63cSGunnar Mills /* req.data = buf.slice(28); */ 308418db63cSGunnar Mills 309418db63cSGunnar Mills var err = 0; 310418db63cSGunnar Mills var consumed = 28; 311418db63cSGunnar Mills 312418db63cSGunnar Mills /* the command handlers return 0 on success, and send their 313418db63cSGunnar Mills * own response. Otherwise, a non-zero error code will be 314418db63cSGunnar Mills * used as a simple error response 315418db63cSGunnar Mills */ 316418db63cSGunnar Mills switch (req.type) { 317418db63cSGunnar Mills case NBD_CMD_READ: 318418db63cSGunnar Mills err = this._handle_cmd_read(req); 319418db63cSGunnar Mills break; 320418db63cSGunnar Mills 321418db63cSGunnar Mills case NBD_CMD_DISC: 322418db63cSGunnar Mills err = this._handle_cmd_disconnect(req); 323418db63cSGunnar Mills break; 324418db63cSGunnar Mills 325418db63cSGunnar Mills case NBD_CMD_WRITE: 326418db63cSGunnar Mills /* we also need length bytes of data to consume a write 327418db63cSGunnar Mills * request */ 328418db63cSGunnar Mills if (buf.byteLength < 28 + req.length) { 329418db63cSGunnar Mills return 0; 330418db63cSGunnar Mills } 331418db63cSGunnar Mills consumed += req.length; 332418db63cSGunnar Mills err = EPERM; 333418db63cSGunnar Mills break; 334418db63cSGunnar Mills 335418db63cSGunnar Mills case NBD_CMD_TRIM: 336418db63cSGunnar Mills err = EPERM; 337418db63cSGunnar Mills break; 338418db63cSGunnar Mills 339418db63cSGunnar Mills default: 340418db63cSGunnar Mills console.log('invalid command 0x' + req.type.toString(16)); 341418db63cSGunnar Mills err = EINVAL; 342418db63cSGunnar Mills } 343418db63cSGunnar Mills 344418db63cSGunnar Mills if (err) { 345418db63cSGunnar Mills console.log('error handle_cmd: ' + err); 346418db63cSGunnar Mills var resp = this._create_cmd_response(req, err); 347418db63cSGunnar Mills this.ws.send(resp); 348418db63cSGunnar Mills } 349418db63cSGunnar Mills 350418db63cSGunnar Mills return consumed; 351418db63cSGunnar Mills }; 352418db63cSGunnar Mills 353418db63cSGunnar Mills this._handle_cmd_read = function(req) { 354418db63cSGunnar Mills var offset; 355418db63cSGunnar Mills 356418db63cSGunnar Mills offset = (req.offset_msB * 2 ** 32) + req.offset_lsB; 357418db63cSGunnar Mills 358418db63cSGunnar Mills if (offset > Number.MAX_SAFE_INTEGER) return ENOSPC; 359418db63cSGunnar Mills 360418db63cSGunnar Mills if (offset + req.length > Number.MAX_SAFE_INTEGER) return ENOSPC; 361418db63cSGunnar Mills 362418db63cSGunnar Mills if (offset + req.length > file.size) return ENOSPC; 363418db63cSGunnar Mills 364418db63cSGunnar Mills var blob = this.file.slice(offset, offset + req.length); 365418db63cSGunnar Mills var reader = new FileReader(); 366418db63cSGunnar Mills 367418db63cSGunnar Mills reader.onload = (function(ev) { 368418db63cSGunnar Mills var reader = ev.target; 369418db63cSGunnar Mills if (reader.readyState != FileReader.DONE) return; 370418db63cSGunnar Mills var resp = 371418db63cSGunnar Mills this._create_cmd_response(req, 0, reader.result); 372418db63cSGunnar Mills this.ws.send(resp); 373418db63cSGunnar Mills }).bind(this); 374418db63cSGunnar Mills 375418db63cSGunnar Mills reader.onerror = (function(ev) { 376418db63cSGunnar Mills var reader = ev.target; 377418db63cSGunnar Mills console.log('error reading file: ' + reader.error); 378418db63cSGunnar Mills var resp = this._create_cmd_response(req, EIO); 379418db63cSGunnar Mills this.ws.send(resp); 380418db63cSGunnar Mills }).bind(this); 381418db63cSGunnar Mills reader.readAsArrayBuffer(blob); 382418db63cSGunnar Mills 383418db63cSGunnar Mills return 0; 384418db63cSGunnar Mills }; 385418db63cSGunnar Mills 386418db63cSGunnar Mills this._handle_cmd_disconnect = function(req) { 387418db63cSGunnar Mills this.stop(); 388418db63cSGunnar Mills return 0; 389418db63cSGunnar Mills }; 390418db63cSGunnar Mills 391418db63cSGunnar Mills this.recv_handlers = Object.freeze({ 392418db63cSGunnar Mills [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this), 393418db63cSGunnar Mills [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this), 394418db63cSGunnar Mills [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this), 395418db63cSGunnar Mills }); 396418db63cSGunnar Mills} 397