blob: 1f6f88fb477f1f8ebcd274d57e9d6f4151daa8ba [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;
160 // eslint-disable-next-line prettier/prettier
161 view.setUint32(0, Math.floor(size / (2 ** 32)));
162 view.setUint32(4, size & 0xffffffff);
163 /* transmission flags: read-only */
164 view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY);
165 this.ws.send(resp);
166 this.state = NBD_STATE_TRANSMISSION;
167 break;
168 default:
169 console.log('handle_option: Unsupported option: ' + opt);
170 /* reject other options */
171 var resp1 = new ArrayBuffer(20);
172 var view1 = new DataView(resp1, 0, 20);
173 view1.setUint32(0, 0x0003e889);
174 view1.setUint32(4, 0x045565a9);
175 view1.setUint32(8, opt);
176 view1.setUint32(12, NBD_REP_ERR_UNSUP);
177 view1.setUint32(16, 0);
178 this.ws.send(resp1);
179 }
180 return 16 + len;
181 };
Derick Montague602e98a2020-10-21 16:20:00 -0500182 this._create_cmd_response = function (req, rc, data = null) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200183 var len = 16;
184 if (data) len += data.byteLength;
185 var resp = new ArrayBuffer(len);
186 var view = new DataView(resp, 0, 16);
187 view.setUint32(0, 0x67446698);
188 view.setUint32(4, rc);
189 view.setUint32(8, req.handle_msB);
190 view.setUint32(12, req.handle_lsB);
191 if (data) new Uint8Array(resp, 16).set(new Uint8Array(data));
192 return resp;
193 };
Derick Montague602e98a2020-10-21 16:20:00 -0500194 this._handle_cmd = function (buf) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200195 if (buf.byteLength < 28) {
196 return 0;
197 }
198 var view = new DataView(buf, 0, 28);
199 if (view.getUint32(0) != 0x25609513) {
200 console.log('invalid request magic');
201 return -1;
202 }
203 var req = {
204 flags: view.getUint16(4),
205 type: view.getUint16(6),
206 handle_msB: view.getUint32(8),
207 handle_lsB: view.getUint32(12),
208 offset_msB: view.getUint32(16),
209 offset_lsB: view.getUint32(20),
Derick Montague602e98a2020-10-21 16:20:00 -0500210 length: view.getUint32(24),
Mateusz Gapski75100462020-07-30 11:01:29 +0200211 };
212 /* we don't support writes, so nothing needs the data at present */
213 /* req.data = buf.slice(28); */
214 var err = 0;
215 var consumed = 28;
216 /* the command handlers return 0 on success, and send their
217 * own response. Otherwise, a non-zero error code will be
218 * used as a simple error response
219 */
220 switch (req.type) {
221 case NBD_CMD_READ:
222 err = this._handle_cmd_read(req);
223 break;
224 case NBD_CMD_DISC:
225 err = this._handle_cmd_disconnect(req);
226 break;
227 case NBD_CMD_WRITE:
228 /* we also need length bytes of data to consume a write
229 * request */
230 if (buf.byteLength < 28 + req.length) {
231 return 0;
232 }
233 consumed += req.length;
234 err = EPERM;
235 break;
236 case NBD_CMD_TRIM:
237 err = EPERM;
238 break;
239 default:
240 console.log('invalid command 0x' + req.type.toString(16));
241 err = EINVAL;
242 }
243 if (err) {
244 console.log('error handle_cmd: ' + err);
245 var resp = this._create_cmd_response(req, err);
246 this.ws.send(resp);
247 if (err == ENOSPC) {
248 this.errorReadingFile();
249 this.stop();
250 }
251 }
252 return consumed;
253 };
Derick Montague602e98a2020-10-21 16:20:00 -0500254 this._handle_cmd_read = function (req) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200255 var offset;
256 // eslint-disable-next-line prettier/prettier
257 offset = (req.offset_msB * 2 ** 32) + req.offset_lsB;
258 if (offset > Number.MAX_SAFE_INTEGER) return ENOSPC;
259 if (offset + req.length > Number.MAX_SAFE_INTEGER) return ENOSPC;
260 if (offset + req.length > file.size) return ENOSPC;
261 var blob = this.file.slice(offset, offset + req.length);
262 var reader = new FileReader();
263
Derick Montague602e98a2020-10-21 16:20:00 -0500264 reader.onload = function (ev) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200265 var reader = ev.target;
266 if (reader.readyState != FileReader.DONE) return;
267 var resp = this._create_cmd_response(req, 0, reader.result);
268 this.ws.send(resp);
269 }.bind(this);
270
Derick Montague602e98a2020-10-21 16:20:00 -0500271 reader.onerror = function (ev) {
Mateusz Gapski75100462020-07-30 11:01:29 +0200272 var reader = ev.target;
273 console.log('error reading file: ' + reader.error);
274 var resp = this._create_cmd_response(req, EIO);
275 this.ws.send(resp);
276 }.bind(this);
277 reader.readAsArrayBuffer(blob);
278 return 0;
279 };
Derick Montague602e98a2020-10-21 16:20:00 -0500280 this._handle_cmd_disconnect = function () {
Mateusz Gapski75100462020-07-30 11:01:29 +0200281 this.stop();
282 return 0;
283 };
284 this.recv_handlers = Object.freeze({
285 [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this),
286 [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this),
Derick Montague602e98a2020-10-21 16:20:00 -0500287 [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this),
Mateusz Gapski75100462020-07-30 11:01:29 +0200288 });
289 }
290}