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