| /** |
| * Controller for virtual-media |
| * |
| * @module app/serverControl |
| * @exports virtualMediaController |
| * @name virtualMediaController |
| */ |
| |
| window.angular && (function(angular) { |
| 'use strict'; |
| |
| angular.module('app.serverControl').controller('virtualMediaController', [ |
| '$scope', 'APIUtils', 'toastService', 'dataService', 'nbdServerService', |
| function($scope, APIUtils, toastService, dataService, nbdServerService) { |
| $scope.devices = []; |
| |
| // Only one Virtual Media WebSocket device is currently available. |
| // Path is /vm/0/0. |
| // TODO: Support more than 1 VM device, when backend support is added. |
| var vmDevice = {}; |
| // Hardcode to 0 since /vm/0/0. Last 0 is the device ID. |
| // To support more than 1 device ID, replace with a call to get the |
| // device IDs and names. |
| vmDevice.id = 0; |
| vmDevice.deviceName = 'Virtual media device'; |
| findExistingConnection(vmDevice); |
| $scope.devices.push(vmDevice); |
| |
| $scope.startVM = function(index) { |
| $scope.devices[index].isActive = true; |
| var file = $scope.devices[index].file; |
| var id = $scope.devices[index].id; |
| var host = dataService.getHost().replace('https://', ''); |
| var server = new NBDServer('wss://' + host + '/vm/0/' + id, file, id); |
| $scope.devices[index].nbdServer = server; |
| nbdServerService.addConnection(id, server, file); |
| server.start(); |
| }; |
| $scope.stopVM = function(index) { |
| $scope.devices[index].isActive = false; |
| var server = $scope.devices[index].nbdServer; |
| server.stop(); |
| }; |
| |
| $scope.resetFile = function(index) { |
| document.getElementById('file-upload').value = ''; |
| $scope.devices[index].file = ''; |
| }; |
| |
| function findExistingConnection(vmDevice) { |
| // Checks with existing connections kept in nbdServerService for an open |
| // Websocket connection. |
| var existingConnectionsMap = nbdServerService.getExistingConnections(); |
| if (existingConnectionsMap.hasOwnProperty(vmDevice.id)) { |
| // Open ws will have a ready state of 1 |
| if (existingConnectionsMap[vmDevice.id].server.ws.readyState === 1) { |
| vmDevice.isActive = true; |
| vmDevice.file = existingConnectionsMap[vmDevice.id].file; |
| vmDevice.nbdServer = existingConnectionsMap[vmDevice.id].server; |
| } |
| } |
| return vmDevice; |
| } |
| } |
| ]); |
| })(angular); |
| |
| /* handshake flags */ |
| const NBD_FLAG_FIXED_NEWSTYLE = 0x1; |
| const NBD_FLAG_NO_ZEROES = 0x2; |
| |
| /* transmission flags */ |
| const NBD_FLAG_HAS_FLAGS = 0x1; |
| const NBD_FLAG_READ_ONLY = 0x2; |
| |
| /* option negotiation */ |
| const NBD_OPT_EXPORT_NAME = 0x1; |
| const NBD_REP_FLAG_ERROR = 0x1 << 31; |
| const NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1; |
| |
| /* command definitions */ |
| const NBD_CMD_READ = 0; |
| const NBD_CMD_WRITE = 1; |
| const NBD_CMD_DISC = 2; |
| const NBD_CMD_TRIM = 4; |
| |
| /* errno */ |
| const EPERM = 1; |
| const EIO = 5; |
| const EINVAL = 22; |
| const ENOSPC = 28; |
| |
| /* internal object state */ |
| const NBD_STATE_UNKNOWN = 1; |
| const NBD_STATE_OPEN = 2; |
| const NBD_STATE_WAIT_CFLAGS = 3; |
| const NBD_STATE_WAIT_OPTION = 4; |
| const NBD_STATE_TRANSMISSION = 5; |
| |
| function NBDServer(endpoint, file, id) { |
| this.file = file; |
| this.id = id; |
| this.endpoint = endpoint; |
| this.ws = null; |
| this.state = NBD_STATE_UNKNOWN; |
| this.msgbuf = null; |
| |
| this.start = function() { |
| this.ws = new WebSocket(this.endpoint); |
| this.state = NBD_STATE_OPEN; |
| this.ws.binaryType = 'arraybuffer'; |
| this.ws.onmessage = this._on_ws_message.bind(this); |
| this.ws.onopen = this._on_ws_open.bind(this); |
| this.ws.onclose = this._on_ws_close.bind(this); |
| this.ws.onerror = this._on_ws_error.bind(this); |
| }; |
| |
| this.stop = function() { |
| this.ws.close(); |
| this.state = NBD_STATE_UNKNOWN; |
| }; |
| |
| this._on_ws_error = function(ev) { |
| console.log('vm/0/' + id + 'error: ' + ev); |
| }; |
| |
| this._on_ws_close = function(ev) { |
| console.log( |
| 'vm/0/' + id + ' closed with code: ' + ev.code + |
| ' reason: ' + ev.reason); |
| }; |
| |
| /* websocket event handlers */ |
| this._on_ws_open = function(ev) { |
| console.log('vm/0/' + id + ' opened'); |
| this.client = { |
| flags: 0, |
| }; |
| this._negotiate(); |
| }; |
| |
| this._on_ws_message = function(ev) { |
| var data = ev.data; |
| |
| if (this.msgbuf == null) { |
| this.msgbuf = data; |
| } else { |
| var tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength); |
| tmp.set(new Uint8Array(this.msgbuf), 0); |
| tmp.set(new Uint8Array(data), this.msgbuf.byteLength); |
| this.msgbuf = tmp.buffer; |
| } |
| |
| for (;;) { |
| var handler = this.recv_handlers[this.state]; |
| if (!handler) { |
| console.log('no handler for state ' + this.state); |
| this.stop(); |
| break; |
| } |
| |
| var consumed = handler(this.msgbuf); |
| if (consumed < 0) { |
| console.log( |
| 'handler[state=' + this.state + '] returned error ' + consumed); |
| this.stop(); |
| break; |
| } |
| |
| if (consumed == 0) { |
| break; |
| } |
| |
| if (consumed > 0) { |
| if (consumed == this.msgbuf.byteLength) { |
| this.msgbuf = null; |
| break; |
| } |
| this.msgbuf = this.msgbuf.slice(consumed); |
| } |
| } |
| }; |
| |
| this._negotiate = function() { |
| var buf = new ArrayBuffer(18); |
| var data = new DataView(buf, 0, 18); |
| |
| /* NBD magic: NBDMAGIC */ |
| data.setUint32(0, 0x4e42444d); |
| data.setUint32(4, 0x41474943); |
| |
| /* newstyle negotiation: IHAVEOPT */ |
| data.setUint32(8, 0x49484156); |
| data.setUint32(12, 0x454F5054); |
| |
| /* flags: fixed newstyle negotiation, no padding */ |
| data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES); |
| |
| this.state = NBD_STATE_WAIT_CFLAGS; |
| this.ws.send(buf); |
| }; |
| |
| /* handlers */ |
| this._handle_cflags = function(buf) { |
| if (buf.byteLength < 4) { |
| return 0; |
| } |
| |
| var data = new DataView(buf, 0, 4); |
| this.client.flags = data.getUint32(0); |
| |
| this.state = NBD_STATE_WAIT_OPTION; |
| return 4; |
| }; |
| |
| this._handle_option = function(buf) { |
| if (buf.byteLength < 16) return 0; |
| |
| var data = new DataView(buf, 0, 16); |
| if (data.getUint32(0) != 0x49484156 || data.getUint32(4) != 0x454F5054) { |
| console.log('invalid option magic'); |
| return -1; |
| } |
| |
| var opt = data.getUint32(8); |
| var len = data.getUint32(12); |
| |
| |
| if (buf.byteLength < 16 + len) { |
| return 0; |
| } |
| |
| switch (opt) { |
| case NBD_OPT_EXPORT_NAME: |
| var n = 10; |
| if (!(this.client.flags & NBD_FLAG_NO_ZEROES)) n += 124; |
| var resp = new ArrayBuffer(n); |
| var view = new DataView(resp, 0, 10); |
| /* export size. */ |
| var size = this.file.size; |
| view.setUint32(0, Math.floor(size / (2 ** 32))); |
| view.setUint32(4, size & 0xffffffff); |
| /* transmission flags: read-only */ |
| view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY); |
| this.ws.send(resp); |
| |
| this.state = NBD_STATE_TRANSMISSION; |
| break; |
| |
| default: |
| console.log('handle_option: Unsupported option: ' + opt); |
| /* reject other options */ |
| var resp = new ArrayBuffer(20); |
| var view = new DataView(resp, 0, 20); |
| view.setUint32(0, 0x0003e889); |
| view.setUint32(4, 0x045565a9); |
| view.setUint32(8, opt); |
| view.setUint32(12, NBD_REP_ERR_UNSUP); |
| view.setUint32(16, 0); |
| this.ws.send(resp); |
| } |
| |
| return 16 + len; |
| }; |
| |
| this._create_cmd_response = function(req, rc, data = null) { |
| var len = 16; |
| if (data) len += data.byteLength; |
| var resp = new ArrayBuffer(len); |
| var view = new DataView(resp, 0, 16); |
| view.setUint32(0, 0x67446698); |
| view.setUint32(4, rc); |
| view.setUint32(8, req.handle_msB); |
| view.setUint32(12, req.handle_lsB); |
| if (data) new Uint8Array(resp, 16).set(new Uint8Array(data)); |
| return resp; |
| }; |
| |
| this._handle_cmd = function(buf) { |
| if (buf.byteLength < 28) { |
| return 0; |
| } |
| |
| var view = new DataView(buf, 0, 28); |
| |
| if (view.getUint32(0) != 0x25609513) { |
| console.log('invalid request magic'); |
| return -1; |
| } |
| |
| var req = { |
| flags: view.getUint16(4), |
| type: view.getUint16(6), |
| handle_msB: view.getUint32(8), |
| handle_lsB: view.getUint32(12), |
| offset_msB: view.getUint32(16), |
| offset_lsB: view.getUint32(20), |
| length: view.getUint32(24), |
| }; |
| |
| /* we don't support writes, so nothing needs the data at present */ |
| /* req.data = buf.slice(28); */ |
| |
| var err = 0; |
| var consumed = 28; |
| |
| /* the command handlers return 0 on success, and send their |
| * own response. Otherwise, a non-zero error code will be |
| * used as a simple error response |
| */ |
| switch (req.type) { |
| case NBD_CMD_READ: |
| err = this._handle_cmd_read(req); |
| break; |
| |
| case NBD_CMD_DISC: |
| err = this._handle_cmd_disconnect(req); |
| break; |
| |
| case NBD_CMD_WRITE: |
| /* we also need length bytes of data to consume a write |
| * request */ |
| if (buf.byteLength < 28 + req.length) { |
| return 0; |
| } |
| consumed += req.length; |
| err = EPERM; |
| break; |
| |
| case NBD_CMD_TRIM: |
| err = EPERM; |
| break; |
| |
| default: |
| console.log('invalid command 0x' + req.type.toString(16)); |
| err = EINVAL; |
| } |
| |
| if (err) { |
| console.log('error handle_cmd: ' + err); |
| var resp = this._create_cmd_response(req, err); |
| this.ws.send(resp); |
| } |
| |
| return consumed; |
| }; |
| |
| this._handle_cmd_read = function(req) { |
| var offset; |
| |
| offset = (req.offset_msB * 2 ** 32) + req.offset_lsB; |
| |
| if (offset > Number.MAX_SAFE_INTEGER) return ENOSPC; |
| |
| if (offset + req.length > Number.MAX_SAFE_INTEGER) return ENOSPC; |
| |
| if (offset + req.length > file.size) return ENOSPC; |
| |
| var blob = this.file.slice(offset, offset + req.length); |
| var reader = new FileReader(); |
| |
| reader.onload = (function(ev) { |
| var reader = ev.target; |
| if (reader.readyState != FileReader.DONE) return; |
| var resp = |
| this._create_cmd_response(req, 0, reader.result); |
| this.ws.send(resp); |
| }).bind(this); |
| |
| reader.onerror = (function(ev) { |
| var reader = ev.target; |
| console.log('error reading file: ' + reader.error); |
| var resp = this._create_cmd_response(req, EIO); |
| this.ws.send(resp); |
| }).bind(this); |
| reader.readAsArrayBuffer(blob); |
| |
| return 0; |
| }; |
| |
| this._handle_cmd_disconnect = function(req) { |
| this.stop(); |
| return 0; |
| }; |
| |
| this.recv_handlers = Object.freeze({ |
| [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this), |
| [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this), |
| [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this), |
| }); |
| } |