175100469SMateusz Gapski/* handshake flags */ 275100469SMateusz Gapskiconst NBD_FLAG_FIXED_NEWSTYLE = 0x1; 375100469SMateusz Gapskiconst NBD_FLAG_NO_ZEROES = 0x2; 475100469SMateusz Gapski 575100469SMateusz Gapski/* transmission flags */ 675100469SMateusz Gapskiconst NBD_FLAG_HAS_FLAGS = 0x1; 775100469SMateusz Gapskiconst NBD_FLAG_READ_ONLY = 0x2; 875100469SMateusz Gapski 975100469SMateusz Gapski/* option negotiation */ 1075100469SMateusz Gapskiconst NBD_OPT_EXPORT_NAME = 0x1; 1175100469SMateusz Gapskiconst NBD_REP_FLAG_ERROR = 0x1 << 31; 1275100469SMateusz Gapskiconst NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1; 1375100469SMateusz Gapski 1475100469SMateusz Gapski/* command definitions */ 1575100469SMateusz Gapskiconst NBD_CMD_READ = 0; 1675100469SMateusz Gapskiconst NBD_CMD_WRITE = 1; 1775100469SMateusz Gapskiconst NBD_CMD_DISC = 2; 1875100469SMateusz Gapskiconst NBD_CMD_TRIM = 4; 1975100469SMateusz Gapski 2075100469SMateusz Gapski/* errno */ 2175100469SMateusz Gapskiconst EPERM = 1; 2275100469SMateusz Gapskiconst EIO = 5; 2375100469SMateusz Gapskiconst EINVAL = 22; 2475100469SMateusz Gapskiconst ENOSPC = 28; 2575100469SMateusz Gapski 2675100469SMateusz Gapski/* internal object state */ 2775100469SMateusz Gapskiconst NBD_STATE_UNKNOWN = 1; 2875100469SMateusz Gapskiconst NBD_STATE_OPEN = 2; 2975100469SMateusz Gapskiconst NBD_STATE_WAIT_CFLAGS = 3; 3075100469SMateusz Gapskiconst NBD_STATE_WAIT_OPTION = 4; 3175100469SMateusz Gapskiconst NBD_STATE_TRANSMISSION = 5; 3275100469SMateusz Gapski 3375100469SMateusz Gapskiexport default class NBDServer { 3475100469SMateusz Gapski constructor(endpoint, file, id, token) { 3575100469SMateusz Gapski this.socketStarted = () => {}; 3675100469SMateusz Gapski this.socketClosed = () => {}; 3775100469SMateusz Gapski this.errorReadingFile = () => {}; 3875100469SMateusz Gapski this.file = file; 3975100469SMateusz Gapski this.id = id; 4075100469SMateusz Gapski this.endpoint = endpoint; 4175100469SMateusz Gapski this.ws = null; 4275100469SMateusz Gapski this.state = NBD_STATE_UNKNOWN; 4375100469SMateusz Gapski this.msgbuf = null; 4475100469SMateusz Gapski this.start = function () { 4575100469SMateusz Gapski this.ws = new WebSocket(this.endpoint, [token]); 4675100469SMateusz Gapski this.state = NBD_STATE_OPEN; 4775100469SMateusz Gapski this.ws.binaryType = 'arraybuffer'; 4875100469SMateusz Gapski this.ws.onmessage = this._on_ws_message.bind(this); 4975100469SMateusz Gapski this.ws.onopen = this._on_ws_open.bind(this); 5075100469SMateusz Gapski this.ws.onclose = this._on_ws_close.bind(this); 5175100469SMateusz Gapski this.ws.onerror = this._on_ws_error.bind(this); 5275100469SMateusz Gapski this.socketStarted(); 5375100469SMateusz Gapski }; 5475100469SMateusz Gapski this.stop = function () { 5575100469SMateusz Gapski if (this.ws.readyState == 1) { 5675100469SMateusz Gapski this.ws.close(); 5775100469SMateusz Gapski this.state = NBD_STATE_UNKNOWN; 5875100469SMateusz Gapski } 5975100469SMateusz Gapski }; 6075100469SMateusz Gapski this._on_ws_error = function (ev) { 6175100469SMateusz Gapski console.log(`${endpoint} error: ${ev.error}`); 6275100469SMateusz Gapski console.log(JSON.stringify(ev)); 6375100469SMateusz Gapski }; 6475100469SMateusz Gapski this._on_ws_close = function (ev) { 6575100469SMateusz Gapski console.log( 66*8132399cSEd Tanous `${endpoint} closed with code: ${ev.code} + reason: ${ev.reason}`, 6775100469SMateusz Gapski ); 6875100469SMateusz Gapski console.log(JSON.stringify(ev)); 6975100469SMateusz Gapski this.socketClosed(ev.code); 7075100469SMateusz Gapski }; 7175100469SMateusz Gapski /* websocket event handlers */ 7275100469SMateusz Gapski this._on_ws_open = function () { 7375100469SMateusz Gapski console.log(endpoint + ' opened'); 7475100469SMateusz Gapski this.client = { 75602e98aaSDerick Montague flags: 0, 7675100469SMateusz Gapski }; 7775100469SMateusz Gapski this._negotiate(); 7875100469SMateusz Gapski }; 7975100469SMateusz Gapski this._on_ws_message = function (ev) { 8075100469SMateusz Gapski var data = ev.data; 8175100469SMateusz Gapski if (this.msgbuf == null) { 8275100469SMateusz Gapski this.msgbuf = data; 8375100469SMateusz Gapski } else { 8475100469SMateusz Gapski const tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength); 8575100469SMateusz Gapski tmp.set(new Uint8Array(this.msgbuf), 0); 8675100469SMateusz Gapski tmp.set(new Uint8Array(data), this.msgbuf.byteLength); 8775100469SMateusz Gapski this.msgbuf = tmp.buffer; 8875100469SMateusz Gapski } 8975100469SMateusz Gapski for (;;) { 9075100469SMateusz Gapski var handler = this.recv_handlers[this.state]; 9175100469SMateusz Gapski if (!handler) { 9275100469SMateusz Gapski console.log('no handler for state ' + this.state); 9375100469SMateusz Gapski this.stop(); 9475100469SMateusz Gapski break; 9575100469SMateusz Gapski } 9675100469SMateusz Gapski var consumed = handler(this.msgbuf); 9775100469SMateusz Gapski if (consumed < 0) { 9875100469SMateusz Gapski console.log( 99*8132399cSEd Tanous 'handler[state=' + this.state + '] returned error ' + consumed, 10075100469SMateusz Gapski ); 10175100469SMateusz Gapski this.stop(); 10275100469SMateusz Gapski break; 10375100469SMateusz Gapski } 10475100469SMateusz Gapski if (consumed == 0) { 10575100469SMateusz Gapski break; 10675100469SMateusz Gapski } 10775100469SMateusz Gapski if (consumed > 0) { 10875100469SMateusz Gapski if (consumed == this.msgbuf.byteLength) { 10975100469SMateusz Gapski this.msgbuf = null; 11075100469SMateusz Gapski break; 11175100469SMateusz Gapski } 11275100469SMateusz Gapski this.msgbuf = this.msgbuf.slice(consumed); 11375100469SMateusz Gapski } 11475100469SMateusz Gapski } 11575100469SMateusz Gapski }; 11675100469SMateusz Gapski this._negotiate = function () { 11775100469SMateusz Gapski var buf = new ArrayBuffer(18); 11875100469SMateusz Gapski var data = new DataView(buf, 0, 18); 11975100469SMateusz Gapski /* NBD magic: NBDMAGIC */ 12075100469SMateusz Gapski data.setUint32(0, 0x4e42444d); 12175100469SMateusz Gapski data.setUint32(4, 0x41474943); 12275100469SMateusz Gapski /* newstyle negotiation: IHAVEOPT */ 12375100469SMateusz Gapski data.setUint32(8, 0x49484156); 12475100469SMateusz Gapski data.setUint32(12, 0x454f5054); 12575100469SMateusz Gapski /* flags: fixed newstyle negotiation, no padding */ 12675100469SMateusz Gapski data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES); 12775100469SMateusz Gapski this.state = NBD_STATE_WAIT_CFLAGS; 12875100469SMateusz Gapski this.ws.send(buf); 12975100469SMateusz Gapski }; 13075100469SMateusz Gapski /* handlers */ 13175100469SMateusz Gapski this._handle_cflags = function (buf) { 13275100469SMateusz Gapski if (buf.byteLength < 4) { 13375100469SMateusz Gapski return 0; 13475100469SMateusz Gapski } 13575100469SMateusz Gapski var data = new DataView(buf, 0, 4); 13675100469SMateusz Gapski this.client.flags = data.getUint32(0); 13775100469SMateusz Gapski this.state = NBD_STATE_WAIT_OPTION; 13875100469SMateusz Gapski return 4; 13975100469SMateusz Gapski }; 14075100469SMateusz Gapski this._handle_option = function (buf) { 14175100469SMateusz Gapski if (buf.byteLength < 16) return 0; 14275100469SMateusz Gapski var data = new DataView(buf, 0, 16); 14375100469SMateusz Gapski if (data.getUint32(0) != 0x49484156 || data.getUint32(4) != 0x454f5054) { 14475100469SMateusz Gapski console.log('invalid option magic'); 14575100469SMateusz Gapski return -1; 14675100469SMateusz Gapski } 14775100469SMateusz Gapski var opt = data.getUint32(8); 14875100469SMateusz Gapski var len = data.getUint32(12); 14975100469SMateusz Gapski if (buf.byteLength < 16 + len) { 15075100469SMateusz Gapski return 0; 15175100469SMateusz Gapski } 15275100469SMateusz Gapski switch (opt) { 15375100469SMateusz Gapski case NBD_OPT_EXPORT_NAME: 15475100469SMateusz Gapski var n = 10; 15575100469SMateusz Gapski if (!(this.client.flags & NBD_FLAG_NO_ZEROES)) n += 124; 15675100469SMateusz Gapski var resp = new ArrayBuffer(n); 15775100469SMateusz Gapski var view = new DataView(resp, 0, 10); 15875100469SMateusz Gapski /* export size. */ 15975100469SMateusz Gapski var size = this.file.size; 16075100469SMateusz Gapski // eslint-disable-next-line prettier/prettier 16175100469SMateusz Gapski view.setUint32(0, Math.floor(size / (2 ** 32))); 16275100469SMateusz Gapski view.setUint32(4, size & 0xffffffff); 16375100469SMateusz Gapski /* transmission flags: read-only */ 16475100469SMateusz Gapski view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY); 16575100469SMateusz Gapski this.ws.send(resp); 16675100469SMateusz Gapski this.state = NBD_STATE_TRANSMISSION; 16775100469SMateusz Gapski break; 16875100469SMateusz Gapski default: 16975100469SMateusz Gapski console.log('handle_option: Unsupported option: ' + opt); 17075100469SMateusz Gapski /* reject other options */ 17175100469SMateusz Gapski var resp1 = new ArrayBuffer(20); 17275100469SMateusz Gapski var view1 = new DataView(resp1, 0, 20); 17375100469SMateusz Gapski view1.setUint32(0, 0x0003e889); 17475100469SMateusz Gapski view1.setUint32(4, 0x045565a9); 17575100469SMateusz Gapski view1.setUint32(8, opt); 17675100469SMateusz Gapski view1.setUint32(12, NBD_REP_ERR_UNSUP); 17775100469SMateusz Gapski view1.setUint32(16, 0); 17875100469SMateusz Gapski this.ws.send(resp1); 17975100469SMateusz Gapski } 18075100469SMateusz Gapski return 16 + len; 18175100469SMateusz Gapski }; 18275100469SMateusz Gapski this._create_cmd_response = function (req, rc, data = null) { 18375100469SMateusz Gapski var len = 16; 18475100469SMateusz Gapski if (data) len += data.byteLength; 18575100469SMateusz Gapski var resp = new ArrayBuffer(len); 18675100469SMateusz Gapski var view = new DataView(resp, 0, 16); 18775100469SMateusz Gapski view.setUint32(0, 0x67446698); 18875100469SMateusz Gapski view.setUint32(4, rc); 18975100469SMateusz Gapski view.setUint32(8, req.handle_msB); 19075100469SMateusz Gapski view.setUint32(12, req.handle_lsB); 19175100469SMateusz Gapski if (data) new Uint8Array(resp, 16).set(new Uint8Array(data)); 19275100469SMateusz Gapski return resp; 19375100469SMateusz Gapski }; 19475100469SMateusz Gapski this._handle_cmd = function (buf) { 19575100469SMateusz Gapski if (buf.byteLength < 28) { 19675100469SMateusz Gapski return 0; 19775100469SMateusz Gapski } 19875100469SMateusz Gapski var view = new DataView(buf, 0, 28); 19975100469SMateusz Gapski if (view.getUint32(0) != 0x25609513) { 20075100469SMateusz Gapski console.log('invalid request magic'); 20175100469SMateusz Gapski return -1; 20275100469SMateusz Gapski } 20375100469SMateusz Gapski var req = { 20475100469SMateusz Gapski flags: view.getUint16(4), 20575100469SMateusz Gapski type: view.getUint16(6), 20675100469SMateusz Gapski handle_msB: view.getUint32(8), 20775100469SMateusz Gapski handle_lsB: view.getUint32(12), 20875100469SMateusz Gapski offset_msB: view.getUint32(16), 20975100469SMateusz Gapski offset_lsB: view.getUint32(20), 210602e98aaSDerick Montague length: view.getUint32(24), 21175100469SMateusz Gapski }; 21275100469SMateusz Gapski /* we don't support writes, so nothing needs the data at present */ 21375100469SMateusz Gapski /* req.data = buf.slice(28); */ 21475100469SMateusz Gapski var err = 0; 21575100469SMateusz Gapski var consumed = 28; 21675100469SMateusz Gapski /* the command handlers return 0 on success, and send their 21775100469SMateusz Gapski * own response. Otherwise, a non-zero error code will be 21875100469SMateusz Gapski * used as a simple error response 21975100469SMateusz Gapski */ 22075100469SMateusz Gapski switch (req.type) { 22175100469SMateusz Gapski case NBD_CMD_READ: 22275100469SMateusz Gapski err = this._handle_cmd_read(req); 22375100469SMateusz Gapski break; 22475100469SMateusz Gapski case NBD_CMD_DISC: 22575100469SMateusz Gapski err = this._handle_cmd_disconnect(req); 22675100469SMateusz Gapski break; 22775100469SMateusz Gapski case NBD_CMD_WRITE: 22875100469SMateusz Gapski /* we also need length bytes of data to consume a write 22975100469SMateusz Gapski * request */ 23075100469SMateusz Gapski if (buf.byteLength < 28 + req.length) { 23175100469SMateusz Gapski return 0; 23275100469SMateusz Gapski } 23375100469SMateusz Gapski consumed += req.length; 23475100469SMateusz Gapski err = EPERM; 23575100469SMateusz Gapski break; 23675100469SMateusz Gapski case NBD_CMD_TRIM: 23775100469SMateusz Gapski err = EPERM; 23875100469SMateusz Gapski break; 23975100469SMateusz Gapski default: 24075100469SMateusz Gapski console.log('invalid command 0x' + req.type.toString(16)); 24175100469SMateusz Gapski err = EINVAL; 24275100469SMateusz Gapski } 24375100469SMateusz Gapski if (err) { 24475100469SMateusz Gapski console.log('error handle_cmd: ' + err); 24575100469SMateusz Gapski var resp = this._create_cmd_response(req, err); 24675100469SMateusz Gapski this.ws.send(resp); 24775100469SMateusz Gapski if (err == ENOSPC) { 24875100469SMateusz Gapski this.errorReadingFile(); 24975100469SMateusz Gapski this.stop(); 25075100469SMateusz Gapski } 25175100469SMateusz Gapski } 25275100469SMateusz Gapski return consumed; 25375100469SMateusz Gapski }; 25475100469SMateusz Gapski this._handle_cmd_read = function (req) { 25575100469SMateusz Gapski var offset; 25675100469SMateusz Gapski // eslint-disable-next-line prettier/prettier 25775100469SMateusz Gapski offset = (req.offset_msB * 2 ** 32) + req.offset_lsB; 25875100469SMateusz Gapski if (offset > Number.MAX_SAFE_INTEGER) return ENOSPC; 25975100469SMateusz Gapski if (offset + req.length > Number.MAX_SAFE_INTEGER) return ENOSPC; 26075100469SMateusz Gapski if (offset + req.length > file.size) return ENOSPC; 26175100469SMateusz Gapski var blob = this.file.slice(offset, offset + req.length); 26275100469SMateusz Gapski var reader = new FileReader(); 26375100469SMateusz Gapski 26475100469SMateusz Gapski reader.onload = function (ev) { 26575100469SMateusz Gapski var reader = ev.target; 26675100469SMateusz Gapski if (reader.readyState != FileReader.DONE) return; 26775100469SMateusz Gapski var resp = this._create_cmd_response(req, 0, reader.result); 26875100469SMateusz Gapski this.ws.send(resp); 26975100469SMateusz Gapski }.bind(this); 27075100469SMateusz Gapski 27175100469SMateusz Gapski reader.onerror = function (ev) { 27275100469SMateusz Gapski var reader = ev.target; 27375100469SMateusz Gapski console.log('error reading file: ' + reader.error); 27475100469SMateusz Gapski var resp = this._create_cmd_response(req, EIO); 27575100469SMateusz Gapski this.ws.send(resp); 27675100469SMateusz Gapski }.bind(this); 27775100469SMateusz Gapski reader.readAsArrayBuffer(blob); 27875100469SMateusz Gapski return 0; 27975100469SMateusz Gapski }; 28075100469SMateusz Gapski this._handle_cmd_disconnect = function () { 28175100469SMateusz Gapski this.stop(); 28275100469SMateusz Gapski return 0; 28375100469SMateusz Gapski }; 28475100469SMateusz Gapski this.recv_handlers = Object.freeze({ 28575100469SMateusz Gapski [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this), 28675100469SMateusz Gapski [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this), 287602e98aaSDerick Montague [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this), 28875100469SMateusz Gapski }); 28975100469SMateusz Gapski } 29075100469SMateusz Gapski} 291