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