blob: b58d9aa94964e84265aa17bdcf4c6701b81a72f8 [file] [log] [blame]
Jeremy Kerrf403c422018-07-26 12:14:56 +08001/* Copyright 2018 IBM Corp.
2 *
3 * Author: Jeremy Kerr <jk@ozlabs.org>
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6 * use this file except in compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 * License for the specific language governing permissions and limitations
15 * under the License.
16 */
17
18/* handshake flags */
19const NBD_FLAG_FIXED_NEWSTYLE = 0x1;
20const NBD_FLAG_NO_ZEROES = 0x2;
21
22/* transmission flags */
23const NBD_FLAG_HAS_FLAGS = 0x1;
24const NBD_FLAG_READ_ONLY = 0x2;
25
26/* option negotiation */
27const NBD_OPT_EXPORT_NAME = 0x1;
28const NBD_REP_FLAG_ERROR = 0x1 << 31;
29const NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1;
30
31/* command definitions */
32const NBD_CMD_READ = 0;
33const NBD_CMD_WRITE = 1;
34const NBD_CMD_DISC = 2;
35const NBD_CMD_FLUSH = 3;
36const NBD_CMD_TRIM = 4;
37
38/* errno */
39const EPERM = 1;
40const EIO = 5;
41const ENOMEM = 12;
42const EINVAL = 22;
43const ENOSPC = 28;
44const EOVERFLOW = 75;
45const ESHUTDOWN = 108;
46
47/* internal object state */
48const NBD_STATE_UNKNOWN = 1;
49const NBD_STATE_OPEN = 2;
50const NBD_STATE_WAIT_CFLAGS = 3;
51const NBD_STATE_WAIT_OPTION = 4;
52const NBD_STATE_TRANSMISSION = 5;
53
54function NBDServer(endpoint, file)
55{
56 this.file = file;
57 this.endpoint = endpoint;
58 this.ws = null;
59 this.state = NBD_STATE_UNKNOWN;
60 this.msgbuf = null;
61
62 this.start = function()
63 {
64 this.state = NBD_STATE_OPEN;
65 this.ws = new WebSocket(this.endpoint);
66 this.ws.binaryType = 'arraybuffer';
67 this.ws.onmessage = this._on_ws_message.bind(this);
68 this.ws.onopen = this._on_ws_open.bind(this);
69 }
70
71 this.stop = function()
72 {
73 this.ws.close();
74 this.state = NBD_STATE_UNKNOWN;
75 }
76
77 this._log = function(msg)
78 {
79 if (this.onlog)
80 this.onlog(msg);
81 }
82
83 /* websocket event handlers */
84 this._on_ws_open = function(ev)
85 {
86 this.client = {
87 flags: 0,
88 };
89 this._negotiate();
90 }
91
92 this._on_ws_message = function(ev)
93 {
94 var data = ev.data;
95
96 if (this.msgbuf == null) {
97 this.msgbuf = data;
98 } else {
99 var tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength);
100 tmp.set(new Uint8Array(this.msgbuf), 0);
101 tmp.set(new Uint8Array(data), this.msgbuf.byteLength);
102 this.msgbuf = tmp.buffer;
103 }
104
105 for (;;) {
106 var handler = this.recv_handlers[this.state];
107 if (!handler) {
108 this._log("no handler for state " + this.state);
109 this.stop();
110 break;
111 }
112
113 var consumed = handler(this.msgbuf);
114 if (consumed < 0) {
115 this._log("handler[state=" + this.state +
116 "] returned error " + consumed);
117 this.stop();
118 break;
119 }
120
121 if (consumed == 0)
122 break;
123
124 if (consumed > 0) {
125 if (consumed == this.msgbuf.byteLength) {
126 this.msgbuf = null;
127 break;
128 }
129 this.msgbuf = this.msgbuf.slice(consumed);
130 }
131 }
132 }
133
134 this._negotiate = function()
135 {
136 var buf = new ArrayBuffer(18);
137 var data = new DataView(buf, 0, 18);
138
139 /* NBD magic: NBDMAGIC */
140 data.setUint32(0, 0x4e42444d);
141 data.setUint32(4, 0x41474943);
142
143 /* newstyle negotiation: IHAVEOPT */
144 data.setUint32(8, 0x49484156);
145 data.setUint32(12, 0x454F5054);
146
147 /* flags: fixed newstyle negotiation, no padding */
148 data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES);
149
150 this.state = NBD_STATE_WAIT_CFLAGS;
151 this.ws.send(buf);
152 }
153
154 /* handlers */
155 this._handle_cflags = function(buf)
156 {
157 if (buf.byteLength < 4)
158 return 0;
159
160 var data = new DataView(buf, 0, 4);
161 this.client.flags = data.getUint32(0);
162
163 this._log("client flags received: 0x" +
164 this.client.flags.toString(16));
165
166 this.state = NBD_STATE_WAIT_OPTION;
167 return 4;
168 }
169
170 this._handle_option = function(buf)
171 {
172 if (buf.byteLength < 16)
173 return 0;
174
175 var data = new DataView(buf, 0, 16);
176 if (data.getUint32(0) != 0x49484156 ||
177 data.getUint32(4) != 0x454F5054) {
178 this._log("invalid option magic");
179 return -1;
180 }
181
182 var opt = data.getUint32(8);
183 var len = data.getUint32(12);
184
185 this._log("client option received: 0x" + opt.toString(16));
186
187 if (buf.byteLength < 16 + len)
188 return 0;
189
190 switch (opt) {
191 case NBD_OPT_EXPORT_NAME:
192 this._log("negotiation complete, starting transmission mode");
193 var n = 10;
194 if (!(this.client.flags & NBD_FLAG_NO_ZEROES))
195 n += 124;
196 var resp = new ArrayBuffer(n);
197 var view = new DataView(resp, 0, 10);
Jeremy Kerrbcc6cc52019-03-25 16:30:28 +0800198 /* export size. */
199 var size = this.file.size;
200 view.setUint32(0, Math.floor(size / (2**32)));
201 view.setUint32(4, size & 0xffffffff);
Jeremy Kerrf403c422018-07-26 12:14:56 +0800202 /* transmission flags: read-only */
203 view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY);
204 this.ws.send(resp);
205
206 this.state = NBD_STATE_TRANSMISSION;
207 break;
208
209 default:
210 /* reject other options */
211 var resp = new ArrayBuffer(20);
212 var view = new DataView(resp, 0, 20);
213 view.setUint32(0, 0x0003e889);
214 view.setUint32(4, 0x045565a9);
215 view.setUint32(8, opt);
216 view.setUint32(12, NBD_REP_ERR_UNSUP);
217 view.setUint32(16, 0);
218 this.ws.send(resp);
219 }
220
221 return 16 + len;
222 }
223
224 this._create_cmd_response = function(req, rc, data = null)
225 {
226 var len = 16;
227 if (data)
228 len += data.byteLength;
229 var resp = new ArrayBuffer(len);
230 var view = new DataView(resp, 0, 16);
231 view.setUint32(0, 0x67446698);
232 view.setUint32(4, rc);
233 view.setUint32(8, req.handle_msB);
234 view.setUint32(12, req.handle_lsB);
235 if (data)
236 new Uint8Array(resp, 16).set(new Uint8Array(data));
237 return resp;
238 }
239
240 this._handle_cmd = function(buf)
241 {
242 if (buf.byteLength < 28)
243 return 0;
244
245 var view = new DataView(buf, 0, 28);
246
247 if (view.getUint32(0) != 0x25609513) {
248 this._log("invalid request magic");
249 return -1;
250 }
251
252 var req = {
253 flags: view.getUint16(4),
254 type: view.getUint16(6),
255 handle_msB: view.getUint32(8),
256 handle_lsB: view.getUint32(12),
257 offset_msB: view.getUint32(16),
258 offset_lsB: view.getUint32(20),
259 length: view.getUint32(24),
260 };
261
262 /* we don't support writes, so nothing needs the data at present */
263 /* req.data = buf.slice(28); */
264
265 var err = 0;
266 var consumed = 28;
267
268 /* the command handlers return 0 on success, and send their
269 * own response. Otherwise, a non-zero error code will be
270 * used as a simple error response
271 */
272 switch (req.type) {
273 case NBD_CMD_READ:
274 err = this._handle_cmd_read(req);
275 break;
276
277 case NBD_CMD_DISC:
278 err = this._handle_cmd_disconnect(req);
279 break;
280
281 case NBD_CMD_WRITE:
282 /* we also need length bytes of data to consume a write
283 * request */
284 if (buf.byteLength < 28 + req.length)
285 return 0;
286 consumed += req.length;
287 err = EPERM;
288 break;
289
290 case NBD_CMD_TRIM:
291 err = EPERM;
292 break;
293
294 default:
295 this._log("invalid command 0x" + req.type.toString(16));
296 err = EINVAL;
297 }
298
299 if (err) {
300 var resp = this._create_cmd_response(req, err);
301 this.ws.send(resp);
302 }
303
304 return consumed;
305 }
306
307 this._handle_cmd_read = function(req)
308 {
Jeremy Kerrbcc6cc52019-03-25 16:30:28 +0800309 var offset;
310
311 offset = (req.offset_msB * 2**32) + req.offset_lsB;
312
313 if (offset > Number.MAX_SAFE_INTEGER)
Jeremy Kerrf403c422018-07-26 12:14:56 +0800314 return ENOSPC;
315
Jeremy Kerrbcc6cc52019-03-25 16:30:28 +0800316 if (offset + req.length > Number.MAX_SAFE_INTEGER)
317 return ENOSPC;
318
319 if (offset + req.length > file.size)
Jeremy Kerrf403c422018-07-26 12:14:56 +0800320 return ENOSPC;
321
322 this._log("read: 0x" + req.length.toString(16) +
Jeremy Kerrbcc6cc52019-03-25 16:30:28 +0800323 " bytes, offset 0x" + offset.toString(16));
Jeremy Kerrf403c422018-07-26 12:14:56 +0800324
Jeremy Kerrbcc6cc52019-03-25 16:30:28 +0800325 var blob = this.file.slice(offset, offset + req.length);
Jeremy Kerrf403c422018-07-26 12:14:56 +0800326 var reader = new FileReader();
327
328 reader.onload = (function(ev) {
329 var reader = ev.target;
330 if (reader.readyState != FileReader.DONE)
331 return;
332 var resp = this._create_cmd_response(req, 0, reader.result);
333 this.ws.send(resp);
334 }).bind(this);
335
336 reader.onerror = (function(ev) {
337 var reader = ev.target;
338 this._log("error reading file: " + reader.error);
339 var resp = this._create_cmd_response(req, EIO);
340 this.ws.send(resp);
341 }).bind(this);
342
343 reader.readAsArrayBuffer(blob);
344
345 return 0;
346 }
347
348 this._handle_cmd_disconnect = function(req)
349 {
350 this._log("disconnect received");
351 this.stop();
352 return 0;
353 }
354
355 this.recv_handlers = Object.freeze({
356 [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this),
357 [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this),
358 [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this),
359 });
360}
361
362