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