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