blob: ff5799598b04e131f14aea44e1038289252b7b51 [file] [log] [blame]
Mateusz Gapski75100462020-07-30 11:01:29 +02001/* 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;
Derick Montague602e98a2020-10-21 16:20:00 -050044 this.start = function () {
Mateusz Gapski75100462020-07-30 11:01:29 +020045 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 };
Derick Montague602e98a2020-10-21 16:20:00 -050054 this.stop = function () {
Mateusz Gapski75100462020-07-30 11:01:29 +020055 if (this.ws.readyState == 1) {
56 this.ws.close();
57 this.state = NBD_STATE_UNKNOWN;
58 }
59 };
Derick Montague602e98a2020-10-21 16:20:00 -050060 this._on_ws_error = function (ev) {
Mateusz Gapski75100462020-07-30 11:01:29 +020061 console.log(`${endpoint} error: ${ev.error}`);
62 console.log(JSON.stringify(ev));
63 };
Derick Montague602e98a2020-10-21 16:20:00 -050064 this._on_ws_close = function (ev) {
Mateusz Gapski75100462020-07-30 11:01:29 +020065 console.log(
Ed Tanous81323992024-02-27 11:26:24 -080066 `${endpoint} closed with code: ${ev.code} + reason: ${ev.reason}`,
Mateusz Gapski75100462020-07-30 11:01:29 +020067 );
68 console.log(JSON.stringify(ev));
69 this.socketClosed(ev.code);
70 };
71 /* websocket event handlers */
Derick Montague602e98a2020-10-21 16:20:00 -050072 this._on_ws_open = function () {
Mateusz Gapski75100462020-07-30 11:01:29 +020073 console.log(endpoint + ' opened');
74 this.client = {
Derick Montague602e98a2020-10-21 16:20:00 -050075 flags: 0,
Mateusz Gapski75100462020-07-30 11:01:29 +020076 };
77 this._negotiate();
78 };
Derick Montague602e98a2020-10-21 16:20:00 -050079 this._on_ws_message = function (ev) {
Mateusz Gapski75100462020-07-30 11:01:29 +020080 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(
Ed Tanous81323992024-02-27 11:26:24 -080099 'handler[state=' + this.state + '] returned error ' + consumed,
Mateusz Gapski75100462020-07-30 11:01:29 +0200100 );
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 };
Derick Montague602e98a2020-10-21 16:20:00 -0500116 this._negotiate = function () {
Mateusz Gapski75100462020-07-30 11:01:29 +0200117 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 */
Derick Montague602e98a2020-10-21 16:20:00 -0500131 this._handle_cflags = function (buf) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200132 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 };
Derick Montague602e98a2020-10-21 16:20:00 -0500140 this._handle_option = function (buf) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200141 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;
jason westoverd36ac8a2025-11-03 20:58:59 -0600160 view.setUint32(0, size >>> 32);
Mateusz Gapski75100462020-07-30 11:01:29 +0200161 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 };
Derick Montague602e98a2020-10-21 16:20:00 -0500181 this._create_cmd_response = function (req, rc, data = null) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200182 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 };
Derick Montague602e98a2020-10-21 16:20:00 -0500193 this._handle_cmd = function (buf) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200194 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),
Derick Montague602e98a2020-10-21 16:20:00 -0500209 length: view.getUint32(24),
Mateusz Gapski75100462020-07-30 11:01:29 +0200210 };
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 };
Derick Montague602e98a2020-10-21 16:20:00 -0500253 this._handle_cmd_read = function (req) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200254 var offset;
jason westoverd36ac8a2025-11-03 20:58:59 -0600255 offset = req.offset_msB * 0x100000000 + req.offset_lsB;
Mateusz Gapski75100462020-07-30 11:01:29 +0200256 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
Derick Montague602e98a2020-10-21 16:20:00 -0500262 reader.onload = function (ev) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200263 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
Derick Montague602e98a2020-10-21 16:20:00 -0500269 reader.onerror = function (ev) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200270 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 };
Derick Montague602e98a2020-10-21 16:20:00 -0500278 this._handle_cmd_disconnect = function () {
Mateusz Gapski75100462020-07-30 11:01:29 +0200279 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),
Derick Montague602e98a2020-10-21 16:20:00 -0500285 [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this),
Mateusz Gapski75100462020-07-30 11:01:29 +0200286 });
287 }
288}