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