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