Add VirtualMedia page

More info: https://github.com/openbmc/webui-vue/issues/7

Signed-off-by: Mateusz Gapski <mateuszx.gapski@intel.com>
Change-Id: I68f2074e77301c68c425f1e661988c751224b713
diff --git a/src/utilities/NBDServer.js b/src/utilities/NBDServer.js
new file mode 100644
index 0000000..7c0419a
--- /dev/null
+++ b/src/utilities/NBDServer.js
@@ -0,0 +1,290 @@
+/* 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;
+
+export default class NBDServer {
+  constructor(endpoint, file, id, token) {
+    this.socketStarted = () => {};
+    this.socketClosed = () => {};
+    this.errorReadingFile = () => {};
+    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, [token]);
+      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.socketStarted();
+    };
+    this.stop = function() {
+      if (this.ws.readyState == 1) {
+        this.ws.close();
+        this.state = NBD_STATE_UNKNOWN;
+      }
+    };
+    this._on_ws_error = function(ev) {
+      console.log(`${endpoint} error: ${ev.error}`);
+      console.log(JSON.stringify(ev));
+    };
+    this._on_ws_close = function(ev) {
+      console.log(
+        `${endpoint} closed with code: ${ev.code} + reason: ${ev.reason}`
+      );
+      console.log(JSON.stringify(ev));
+      this.socketClosed(ev.code);
+    };
+    /* websocket event handlers */
+    this._on_ws_open = function() {
+      console.log(endpoint + ' 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 {
+        const 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;
+          // eslint-disable-next-line prettier/prettier
+          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 resp1 = new ArrayBuffer(20);
+          var view1 = new DataView(resp1, 0, 20);
+          view1.setUint32(0, 0x0003e889);
+          view1.setUint32(4, 0x045565a9);
+          view1.setUint32(8, opt);
+          view1.setUint32(12, NBD_REP_ERR_UNSUP);
+          view1.setUint32(16, 0);
+          this.ws.send(resp1);
+      }
+      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);
+        if (err == ENOSPC) {
+          this.errorReadingFile();
+          this.stop();
+        }
+      }
+      return consumed;
+    };
+    this._handle_cmd_read = function(req) {
+      var offset;
+      // eslint-disable-next-line prettier/prettier
+      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() {
+      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)
+    });
+  }
+}