1*75100469SMateusz Gapski/* handshake flags */ 2*75100469SMateusz Gapskiconst NBD_FLAG_FIXED_NEWSTYLE = 0x1; 3*75100469SMateusz Gapskiconst NBD_FLAG_NO_ZEROES = 0x2; 4*75100469SMateusz Gapski 5*75100469SMateusz Gapski/* transmission flags */ 6*75100469SMateusz Gapskiconst NBD_FLAG_HAS_FLAGS = 0x1; 7*75100469SMateusz Gapskiconst NBD_FLAG_READ_ONLY = 0x2; 8*75100469SMateusz Gapski 9*75100469SMateusz Gapski/* option negotiation */ 10*75100469SMateusz Gapskiconst NBD_OPT_EXPORT_NAME = 0x1; 11*75100469SMateusz Gapskiconst NBD_REP_FLAG_ERROR = 0x1 << 31; 12*75100469SMateusz Gapskiconst NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1; 13*75100469SMateusz Gapski 14*75100469SMateusz Gapski/* command definitions */ 15*75100469SMateusz Gapskiconst NBD_CMD_READ = 0; 16*75100469SMateusz Gapskiconst NBD_CMD_WRITE = 1; 17*75100469SMateusz Gapskiconst NBD_CMD_DISC = 2; 18*75100469SMateusz Gapskiconst NBD_CMD_TRIM = 4; 19*75100469SMateusz Gapski 20*75100469SMateusz Gapski/* errno */ 21*75100469SMateusz Gapskiconst EPERM = 1; 22*75100469SMateusz Gapskiconst EIO = 5; 23*75100469SMateusz Gapskiconst EINVAL = 22; 24*75100469SMateusz Gapskiconst ENOSPC = 28; 25*75100469SMateusz Gapski 26*75100469SMateusz Gapski/* internal object state */ 27*75100469SMateusz Gapskiconst NBD_STATE_UNKNOWN = 1; 28*75100469SMateusz Gapskiconst NBD_STATE_OPEN = 2; 29*75100469SMateusz Gapskiconst NBD_STATE_WAIT_CFLAGS = 3; 30*75100469SMateusz Gapskiconst NBD_STATE_WAIT_OPTION = 4; 31*75100469SMateusz Gapskiconst NBD_STATE_TRANSMISSION = 5; 32*75100469SMateusz Gapski 33*75100469SMateusz Gapskiexport default class NBDServer { 34*75100469SMateusz Gapski constructor(endpoint, file, id, token) { 35*75100469SMateusz Gapski this.socketStarted = () => {}; 36*75100469SMateusz Gapski this.socketClosed = () => {}; 37*75100469SMateusz Gapski this.errorReadingFile = () => {}; 38*75100469SMateusz Gapski this.file = file; 39*75100469SMateusz Gapski this.id = id; 40*75100469SMateusz Gapski this.endpoint = endpoint; 41*75100469SMateusz Gapski this.ws = null; 42*75100469SMateusz Gapski this.state = NBD_STATE_UNKNOWN; 43*75100469SMateusz Gapski this.msgbuf = null; 44*75100469SMateusz Gapski this.start = function() { 45*75100469SMateusz Gapski this.ws = new WebSocket(this.endpoint, [token]); 46*75100469SMateusz Gapski this.state = NBD_STATE_OPEN; 47*75100469SMateusz Gapski this.ws.binaryType = 'arraybuffer'; 48*75100469SMateusz Gapski this.ws.onmessage = this._on_ws_message.bind(this); 49*75100469SMateusz Gapski this.ws.onopen = this._on_ws_open.bind(this); 50*75100469SMateusz Gapski this.ws.onclose = this._on_ws_close.bind(this); 51*75100469SMateusz Gapski this.ws.onerror = this._on_ws_error.bind(this); 52*75100469SMateusz Gapski this.socketStarted(); 53*75100469SMateusz Gapski }; 54*75100469SMateusz Gapski this.stop = function() { 55*75100469SMateusz Gapski if (this.ws.readyState == 1) { 56*75100469SMateusz Gapski this.ws.close(); 57*75100469SMateusz Gapski this.state = NBD_STATE_UNKNOWN; 58*75100469SMateusz Gapski } 59*75100469SMateusz Gapski }; 60*75100469SMateusz Gapski this._on_ws_error = function(ev) { 61*75100469SMateusz Gapski console.log(`${endpoint} error: ${ev.error}`); 62*75100469SMateusz Gapski console.log(JSON.stringify(ev)); 63*75100469SMateusz Gapski }; 64*75100469SMateusz Gapski this._on_ws_close = function(ev) { 65*75100469SMateusz Gapski console.log( 66*75100469SMateusz Gapski `${endpoint} closed with code: ${ev.code} + reason: ${ev.reason}` 67*75100469SMateusz Gapski ); 68*75100469SMateusz Gapski console.log(JSON.stringify(ev)); 69*75100469SMateusz Gapski this.socketClosed(ev.code); 70*75100469SMateusz Gapski }; 71*75100469SMateusz Gapski /* websocket event handlers */ 72*75100469SMateusz Gapski this._on_ws_open = function() { 73*75100469SMateusz Gapski console.log(endpoint + ' opened'); 74*75100469SMateusz Gapski this.client = { 75*75100469SMateusz Gapski flags: 0 76*75100469SMateusz Gapski }; 77*75100469SMateusz Gapski this._negotiate(); 78*75100469SMateusz Gapski }; 79*75100469SMateusz Gapski this._on_ws_message = function(ev) { 80*75100469SMateusz Gapski var data = ev.data; 81*75100469SMateusz Gapski if (this.msgbuf == null) { 82*75100469SMateusz Gapski this.msgbuf = data; 83*75100469SMateusz Gapski } else { 84*75100469SMateusz Gapski const tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength); 85*75100469SMateusz Gapski tmp.set(new Uint8Array(this.msgbuf), 0); 86*75100469SMateusz Gapski tmp.set(new Uint8Array(data), this.msgbuf.byteLength); 87*75100469SMateusz Gapski this.msgbuf = tmp.buffer; 88*75100469SMateusz Gapski } 89*75100469SMateusz Gapski for (;;) { 90*75100469SMateusz Gapski var handler = this.recv_handlers[this.state]; 91*75100469SMateusz Gapski if (!handler) { 92*75100469SMateusz Gapski console.log('no handler for state ' + this.state); 93*75100469SMateusz Gapski this.stop(); 94*75100469SMateusz Gapski break; 95*75100469SMateusz Gapski } 96*75100469SMateusz Gapski var consumed = handler(this.msgbuf); 97*75100469SMateusz Gapski if (consumed < 0) { 98*75100469SMateusz Gapski console.log( 99*75100469SMateusz Gapski 'handler[state=' + this.state + '] returned error ' + consumed 100*75100469SMateusz Gapski ); 101*75100469SMateusz Gapski this.stop(); 102*75100469SMateusz Gapski break; 103*75100469SMateusz Gapski } 104*75100469SMateusz Gapski if (consumed == 0) { 105*75100469SMateusz Gapski break; 106*75100469SMateusz Gapski } 107*75100469SMateusz Gapski if (consumed > 0) { 108*75100469SMateusz Gapski if (consumed == this.msgbuf.byteLength) { 109*75100469SMateusz Gapski this.msgbuf = null; 110*75100469SMateusz Gapski break; 111*75100469SMateusz Gapski } 112*75100469SMateusz Gapski this.msgbuf = this.msgbuf.slice(consumed); 113*75100469SMateusz Gapski } 114*75100469SMateusz Gapski } 115*75100469SMateusz Gapski }; 116*75100469SMateusz Gapski this._negotiate = function() { 117*75100469SMateusz Gapski var buf = new ArrayBuffer(18); 118*75100469SMateusz Gapski var data = new DataView(buf, 0, 18); 119*75100469SMateusz Gapski /* NBD magic: NBDMAGIC */ 120*75100469SMateusz Gapski data.setUint32(0, 0x4e42444d); 121*75100469SMateusz Gapski data.setUint32(4, 0x41474943); 122*75100469SMateusz Gapski /* newstyle negotiation: IHAVEOPT */ 123*75100469SMateusz Gapski data.setUint32(8, 0x49484156); 124*75100469SMateusz Gapski data.setUint32(12, 0x454f5054); 125*75100469SMateusz Gapski /* flags: fixed newstyle negotiation, no padding */ 126*75100469SMateusz Gapski data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES); 127*75100469SMateusz Gapski this.state = NBD_STATE_WAIT_CFLAGS; 128*75100469SMateusz Gapski this.ws.send(buf); 129*75100469SMateusz Gapski }; 130*75100469SMateusz Gapski /* handlers */ 131*75100469SMateusz Gapski this._handle_cflags = function(buf) { 132*75100469SMateusz Gapski if (buf.byteLength < 4) { 133*75100469SMateusz Gapski return 0; 134*75100469SMateusz Gapski } 135*75100469SMateusz Gapski var data = new DataView(buf, 0, 4); 136*75100469SMateusz Gapski this.client.flags = data.getUint32(0); 137*75100469SMateusz Gapski this.state = NBD_STATE_WAIT_OPTION; 138*75100469SMateusz Gapski return 4; 139*75100469SMateusz Gapski }; 140*75100469SMateusz Gapski this._handle_option = function(buf) { 141*75100469SMateusz Gapski if (buf.byteLength < 16) return 0; 142*75100469SMateusz Gapski var data = new DataView(buf, 0, 16); 143*75100469SMateusz Gapski if (data.getUint32(0) != 0x49484156 || data.getUint32(4) != 0x454f5054) { 144*75100469SMateusz Gapski console.log('invalid option magic'); 145*75100469SMateusz Gapski return -1; 146*75100469SMateusz Gapski } 147*75100469SMateusz Gapski var opt = data.getUint32(8); 148*75100469SMateusz Gapski var len = data.getUint32(12); 149*75100469SMateusz Gapski if (buf.byteLength < 16 + len) { 150*75100469SMateusz Gapski return 0; 151*75100469SMateusz Gapski } 152*75100469SMateusz Gapski switch (opt) { 153*75100469SMateusz Gapski case NBD_OPT_EXPORT_NAME: 154*75100469SMateusz Gapski var n = 10; 155*75100469SMateusz Gapski if (!(this.client.flags & NBD_FLAG_NO_ZEROES)) n += 124; 156*75100469SMateusz Gapski var resp = new ArrayBuffer(n); 157*75100469SMateusz Gapski var view = new DataView(resp, 0, 10); 158*75100469SMateusz Gapski /* export size. */ 159*75100469SMateusz Gapski var size = this.file.size; 160*75100469SMateusz Gapski // eslint-disable-next-line prettier/prettier 161*75100469SMateusz Gapski view.setUint32(0, Math.floor(size / (2 ** 32))); 162*75100469SMateusz Gapski view.setUint32(4, size & 0xffffffff); 163*75100469SMateusz Gapski /* transmission flags: read-only */ 164*75100469SMateusz Gapski view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY); 165*75100469SMateusz Gapski this.ws.send(resp); 166*75100469SMateusz Gapski this.state = NBD_STATE_TRANSMISSION; 167*75100469SMateusz Gapski break; 168*75100469SMateusz Gapski default: 169*75100469SMateusz Gapski console.log('handle_option: Unsupported option: ' + opt); 170*75100469SMateusz Gapski /* reject other options */ 171*75100469SMateusz Gapski var resp1 = new ArrayBuffer(20); 172*75100469SMateusz Gapski var view1 = new DataView(resp1, 0, 20); 173*75100469SMateusz Gapski view1.setUint32(0, 0x0003e889); 174*75100469SMateusz Gapski view1.setUint32(4, 0x045565a9); 175*75100469SMateusz Gapski view1.setUint32(8, opt); 176*75100469SMateusz Gapski view1.setUint32(12, NBD_REP_ERR_UNSUP); 177*75100469SMateusz Gapski view1.setUint32(16, 0); 178*75100469SMateusz Gapski this.ws.send(resp1); 179*75100469SMateusz Gapski } 180*75100469SMateusz Gapski return 16 + len; 181*75100469SMateusz Gapski }; 182*75100469SMateusz Gapski this._create_cmd_response = function(req, rc, data = null) { 183*75100469SMateusz Gapski var len = 16; 184*75100469SMateusz Gapski if (data) len += data.byteLength; 185*75100469SMateusz Gapski var resp = new ArrayBuffer(len); 186*75100469SMateusz Gapski var view = new DataView(resp, 0, 16); 187*75100469SMateusz Gapski view.setUint32(0, 0x67446698); 188*75100469SMateusz Gapski view.setUint32(4, rc); 189*75100469SMateusz Gapski view.setUint32(8, req.handle_msB); 190*75100469SMateusz Gapski view.setUint32(12, req.handle_lsB); 191*75100469SMateusz Gapski if (data) new Uint8Array(resp, 16).set(new Uint8Array(data)); 192*75100469SMateusz Gapski return resp; 193*75100469SMateusz Gapski }; 194*75100469SMateusz Gapski this._handle_cmd = function(buf) { 195*75100469SMateusz Gapski if (buf.byteLength < 28) { 196*75100469SMateusz Gapski return 0; 197*75100469SMateusz Gapski } 198*75100469SMateusz Gapski var view = new DataView(buf, 0, 28); 199*75100469SMateusz Gapski if (view.getUint32(0) != 0x25609513) { 200*75100469SMateusz Gapski console.log('invalid request magic'); 201*75100469SMateusz Gapski return -1; 202*75100469SMateusz Gapski } 203*75100469SMateusz Gapski var req = { 204*75100469SMateusz Gapski flags: view.getUint16(4), 205*75100469SMateusz Gapski type: view.getUint16(6), 206*75100469SMateusz Gapski handle_msB: view.getUint32(8), 207*75100469SMateusz Gapski handle_lsB: view.getUint32(12), 208*75100469SMateusz Gapski offset_msB: view.getUint32(16), 209*75100469SMateusz Gapski offset_lsB: view.getUint32(20), 210*75100469SMateusz Gapski length: view.getUint32(24) 211*75100469SMateusz Gapski }; 212*75100469SMateusz Gapski /* we don't support writes, so nothing needs the data at present */ 213*75100469SMateusz Gapski /* req.data = buf.slice(28); */ 214*75100469SMateusz Gapski var err = 0; 215*75100469SMateusz Gapski var consumed = 28; 216*75100469SMateusz Gapski /* the command handlers return 0 on success, and send their 217*75100469SMateusz Gapski * own response. Otherwise, a non-zero error code will be 218*75100469SMateusz Gapski * used as a simple error response 219*75100469SMateusz Gapski */ 220*75100469SMateusz Gapski switch (req.type) { 221*75100469SMateusz Gapski case NBD_CMD_READ: 222*75100469SMateusz Gapski err = this._handle_cmd_read(req); 223*75100469SMateusz Gapski break; 224*75100469SMateusz Gapski case NBD_CMD_DISC: 225*75100469SMateusz Gapski err = this._handle_cmd_disconnect(req); 226*75100469SMateusz Gapski break; 227*75100469SMateusz Gapski case NBD_CMD_WRITE: 228*75100469SMateusz Gapski /* we also need length bytes of data to consume a write 229*75100469SMateusz Gapski * request */ 230*75100469SMateusz Gapski if (buf.byteLength < 28 + req.length) { 231*75100469SMateusz Gapski return 0; 232*75100469SMateusz Gapski } 233*75100469SMateusz Gapski consumed += req.length; 234*75100469SMateusz Gapski err = EPERM; 235*75100469SMateusz Gapski break; 236*75100469SMateusz Gapski case NBD_CMD_TRIM: 237*75100469SMateusz Gapski err = EPERM; 238*75100469SMateusz Gapski break; 239*75100469SMateusz Gapski default: 240*75100469SMateusz Gapski console.log('invalid command 0x' + req.type.toString(16)); 241*75100469SMateusz Gapski err = EINVAL; 242*75100469SMateusz Gapski } 243*75100469SMateusz Gapski if (err) { 244*75100469SMateusz Gapski console.log('error handle_cmd: ' + err); 245*75100469SMateusz Gapski var resp = this._create_cmd_response(req, err); 246*75100469SMateusz Gapski this.ws.send(resp); 247*75100469SMateusz Gapski if (err == ENOSPC) { 248*75100469SMateusz Gapski this.errorReadingFile(); 249*75100469SMateusz Gapski this.stop(); 250*75100469SMateusz Gapski } 251*75100469SMateusz Gapski } 252*75100469SMateusz Gapski return consumed; 253*75100469SMateusz Gapski }; 254*75100469SMateusz Gapski this._handle_cmd_read = function(req) { 255*75100469SMateusz Gapski var offset; 256*75100469SMateusz Gapski // eslint-disable-next-line prettier/prettier 257*75100469SMateusz Gapski offset = (req.offset_msB * 2 ** 32) + req.offset_lsB; 258*75100469SMateusz Gapski if (offset > Number.MAX_SAFE_INTEGER) return ENOSPC; 259*75100469SMateusz Gapski if (offset + req.length > Number.MAX_SAFE_INTEGER) return ENOSPC; 260*75100469SMateusz Gapski if (offset + req.length > file.size) return ENOSPC; 261*75100469SMateusz Gapski var blob = this.file.slice(offset, offset + req.length); 262*75100469SMateusz Gapski var reader = new FileReader(); 263*75100469SMateusz Gapski 264*75100469SMateusz Gapski reader.onload = function(ev) { 265*75100469SMateusz Gapski var reader = ev.target; 266*75100469SMateusz Gapski if (reader.readyState != FileReader.DONE) return; 267*75100469SMateusz Gapski var resp = this._create_cmd_response(req, 0, reader.result); 268*75100469SMateusz Gapski this.ws.send(resp); 269*75100469SMateusz Gapski }.bind(this); 270*75100469SMateusz Gapski 271*75100469SMateusz Gapski reader.onerror = function(ev) { 272*75100469SMateusz Gapski var reader = ev.target; 273*75100469SMateusz Gapski console.log('error reading file: ' + reader.error); 274*75100469SMateusz Gapski var resp = this._create_cmd_response(req, EIO); 275*75100469SMateusz Gapski this.ws.send(resp); 276*75100469SMateusz Gapski }.bind(this); 277*75100469SMateusz Gapski reader.readAsArrayBuffer(blob); 278*75100469SMateusz Gapski return 0; 279*75100469SMateusz Gapski }; 280*75100469SMateusz Gapski this._handle_cmd_disconnect = function() { 281*75100469SMateusz Gapski this.stop(); 282*75100469SMateusz Gapski return 0; 283*75100469SMateusz Gapski }; 284*75100469SMateusz Gapski this.recv_handlers = Object.freeze({ 285*75100469SMateusz Gapski [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this), 286*75100469SMateusz Gapski [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this), 287*75100469SMateusz Gapski [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this) 288*75100469SMateusz Gapski }); 289*75100469SMateusz Gapski } 290*75100469SMateusz Gapski} 291