From 75100469ab66851d84082b9651469901136f3d7e Mon Sep 17 00:00:00 2001 From: Mateusz Gapski Date: Thu, 30 Jul 2020 11:01:29 +0200 Subject: Add VirtualMedia page More info: https://github.com/openbmc/webui-vue/issues/7 Signed-off-by: Mateusz Gapski Change-Id: I68f2074e77301c68c425f1e661988c751224b713 --- src/utilities/NBDServer.js | 290 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 src/utilities/NBDServer.js (limited to 'src/utilities') diff --git a/src/utilities/NBDServer.js b/src/utilities/NBDServer.js new file mode 100644 index 00000000..7c0419ae --- /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) + }); + } +} -- cgit v1.2.3