|  | /* 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), | 
|  | }); | 
|  | } | 
|  | } |