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 // 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 }; 182 this._create_cmd_response = function(req, rc, data = null) { 183 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 }; 194 this._handle_cmd = function(buf) { 195 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), 210 length: view.getUint32(24) 211 }; 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 }; 254 this._handle_cmd_read = function(req) { 255 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 264 reader.onload = function(ev) { 265 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 271 reader.onerror = function(ev) { 272 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 }; 280 this._handle_cmd_disconnect = function() { 281 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), 287 [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this) 288 }); 289 } 290} 291