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