xref: /openbmc/webui-vue/src/utilities/NBDServer.js (revision 75100469ab66851d84082b9651469901136f3d7e)
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