blob: 24e945a488355a2be68db4bfc88b00cdd784640f [file] [log] [blame]
beccabroek44da4712019-03-20 13:24:23 -05001/**
2 * Controller for virtual-media
3 *
4 * @module app/configuration
5 * @exports virtualMediaController
6 * @name virtualMediaController
7 */
8
9window.angular && (function(angular) {
10 'use strict';
11
12 angular.module('app.configuration').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}