Initial js/nbd commit
Signed-off-by: Jeremy Kerr <jk@ozlabs.org>
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..7a104c9
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <style type="text/css">
+body, input, #file { font-size: 12px; }
+pre {
+ border: thin solid #c0c0c0;
+ color: #404040;
+ font-family: Inconsolata, Ubuntu mono, fixed;
+ font-size: 9pt;
+ padding: 0.1em 0.4em;
+}
+ </style>
+ <script src="/js/nbd.js" type="text/javascript">
+ </script>
+ <script type="text/javascript">
+var server;
+
+function start_server()
+{
+ var file = document.getElementById("file").files[0];
+
+ server = new NBDServer("ws://" + location.host + "/", file);
+ server.onlog = function(msg) {
+ var container = document.getElementById("log");
+ container.innerText += msg + "\n";
+ }
+ server.start()
+}
+
+function stop_server()
+{
+ if (server)
+ server.stop();
+}
+
+document.addEventListener("DOMContentLoaded", function(event) {
+ document.getElementById("go").focus();
+});
+
+ </script>
+ </head>
+ <body>
+ <div>
+ <input type="file" id="file">
+ <input type="button" id="go" onclick="start_server()" value="Serve Image">
+ <input type="button" id="stop" onclick="stop_server()" value="Stop">
+ </div>
+ <pre id="log"></pre>
+ </body>
+</html>
diff --git a/web/js/nbd.js b/web/js/nbd.js
new file mode 100644
index 0000000..1de4d3b
--- /dev/null
+++ b/web/js/nbd.js
@@ -0,0 +1,355 @@
+/* Copyright 2018 IBM Corp.
+ *
+ * Author: Jeremy Kerr <jk@ozlabs.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* handshake flags */
+const NBD_FLAG_FIXED_NEWSTYLE = 0x1;
+const NBD_FLAG_NO_ZEROES = 0x2;
+
+/* transmission flags */
+const NBD_FLAG_HAS_FLAGS = 0x1;
+const NBD_FLAG_READ_ONLY = 0x2;
+
+/* option negotiation */
+const NBD_OPT_EXPORT_NAME = 0x1;
+const NBD_REP_FLAG_ERROR = 0x1 << 31;
+const NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1;
+
+/* command definitions */
+const NBD_CMD_READ = 0;
+const NBD_CMD_WRITE = 1;
+const NBD_CMD_DISC = 2;
+const NBD_CMD_FLUSH = 3;
+const NBD_CMD_TRIM = 4;
+
+/* errno */
+const EPERM = 1;
+const EIO = 5;
+const ENOMEM = 12;
+const EINVAL = 22;
+const ENOSPC = 28;
+const EOVERFLOW = 75;
+const ESHUTDOWN = 108;
+
+/* internal object state */
+const NBD_STATE_UNKNOWN = 1;
+const NBD_STATE_OPEN = 2;
+const NBD_STATE_WAIT_CFLAGS = 3;
+const NBD_STATE_WAIT_OPTION = 4;
+const NBD_STATE_TRANSMISSION = 5;
+
+function NBDServer(endpoint, file)
+{
+ this.file = file;
+ this.endpoint = endpoint;
+ this.ws = null;
+ this.state = NBD_STATE_UNKNOWN;
+ this.msgbuf = null;
+
+ this.start = function()
+ {
+ this.state = NBD_STATE_OPEN;
+ this.ws = new WebSocket(this.endpoint);
+ this.ws.binaryType = 'arraybuffer';
+ this.ws.onmessage = this._on_ws_message.bind(this);
+ this.ws.onopen = this._on_ws_open.bind(this);
+ }
+
+ this.stop = function()
+ {
+ this.ws.close();
+ this.state = NBD_STATE_UNKNOWN;
+ }
+
+ this._log = function(msg)
+ {
+ if (this.onlog)
+ this.onlog(msg);
+ }
+
+ /* websocket event handlers */
+ this._on_ws_open = function(ev)
+ {
+ this.client = {
+ flags: 0,
+ };
+ this._negotiate();
+ }
+
+ this._on_ws_message = function(ev)
+ {
+ var data = ev.data;
+
+ if (this.msgbuf == null) {
+ this.msgbuf = data;
+ } else {
+ var tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength);
+ tmp.set(new Uint8Array(this.msgbuf), 0);
+ tmp.set(new Uint8Array(data), this.msgbuf.byteLength);
+ this.msgbuf = tmp.buffer;
+ }
+
+ for (;;) {
+ var handler = this.recv_handlers[this.state];
+ if (!handler) {
+ this._log("no handler for state " + this.state);
+ this.stop();
+ break;
+ }
+
+ var consumed = handler(this.msgbuf);
+ if (consumed < 0) {
+ this._log("handler[state=" + this.state +
+ "] returned error " + consumed);
+ this.stop();
+ break;
+ }
+
+ if (consumed == 0)
+ break;
+
+ if (consumed > 0) {
+ if (consumed == this.msgbuf.byteLength) {
+ this.msgbuf = null;
+ break;
+ }
+ this.msgbuf = this.msgbuf.slice(consumed);
+ }
+ }
+ }
+
+ this._negotiate = function()
+ {
+ var buf = new ArrayBuffer(18);
+ var data = new DataView(buf, 0, 18);
+
+ /* NBD magic: NBDMAGIC */
+ data.setUint32(0, 0x4e42444d);
+ data.setUint32(4, 0x41474943);
+
+ /* newstyle negotiation: IHAVEOPT */
+ data.setUint32(8, 0x49484156);
+ data.setUint32(12, 0x454F5054);
+
+ /* flags: fixed newstyle negotiation, no padding */
+ data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES);
+
+ this.state = NBD_STATE_WAIT_CFLAGS;
+ this.ws.send(buf);
+ }
+
+ /* handlers */
+ this._handle_cflags = function(buf)
+ {
+ if (buf.byteLength < 4)
+ return 0;
+
+ var data = new DataView(buf, 0, 4);
+ this.client.flags = data.getUint32(0);
+
+ this._log("client flags received: 0x" +
+ this.client.flags.toString(16));
+
+ this.state = NBD_STATE_WAIT_OPTION;
+ return 4;
+ }
+
+ this._handle_option = function(buf)
+ {
+ if (buf.byteLength < 16)
+ return 0;
+
+ var data = new DataView(buf, 0, 16);
+ if (data.getUint32(0) != 0x49484156 ||
+ data.getUint32(4) != 0x454F5054) {
+ this._log("invalid option magic");
+ return -1;
+ }
+
+ var opt = data.getUint32(8);
+ var len = data.getUint32(12);
+
+ this._log("client option received: 0x" + opt.toString(16));
+
+ if (buf.byteLength < 16 + len)
+ return 0;
+
+ switch (opt) {
+ case NBD_OPT_EXPORT_NAME:
+ this._log("negotiation complete, starting transmission mode");
+ var n = 10;
+ if (!(this.client.flags & NBD_FLAG_NO_ZEROES))
+ n += 124;
+ var resp = new ArrayBuffer(n);
+ var view = new DataView(resp, 0, 10);
+ /* export size. todo: 64 bits? */
+ view.setUint32(0, 0);
+ view.setUint32(4, this.file.size & 0xffffffff);
+ /* transmission flags: read-only */
+ view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY);
+ this.ws.send(resp);
+
+ this.state = NBD_STATE_TRANSMISSION;
+ break;
+
+ default:
+ /* reject other options */
+ var resp = new ArrayBuffer(20);
+ var view = new DataView(resp, 0, 20);
+ view.setUint32(0, 0x0003e889);
+ view.setUint32(4, 0x045565a9);
+ view.setUint32(8, opt);
+ view.setUint32(12, NBD_REP_ERR_UNSUP);
+ view.setUint32(16, 0);
+ this.ws.send(resp);
+ }
+
+ return 16 + len;
+ }
+
+ this._create_cmd_response = function(req, rc, data = null)
+ {
+ var len = 16;
+ if (data)
+ len += data.byteLength;
+ var resp = new ArrayBuffer(len);
+ var view = new DataView(resp, 0, 16);
+ view.setUint32(0, 0x67446698);
+ view.setUint32(4, rc);
+ view.setUint32(8, req.handle_msB);
+ view.setUint32(12, req.handle_lsB);
+ if (data)
+ new Uint8Array(resp, 16).set(new Uint8Array(data));
+ return resp;
+ }
+
+ this._handle_cmd = function(buf)
+ {
+ if (buf.byteLength < 28)
+ return 0;
+
+ var view = new DataView(buf, 0, 28);
+
+ if (view.getUint32(0) != 0x25609513) {
+ this._log("invalid request magic");
+ return -1;
+ }
+
+ var req = {
+ flags: view.getUint16(4),
+ type: view.getUint16(6),
+ handle_msB: view.getUint32(8),
+ handle_lsB: view.getUint32(12),
+ offset_msB: view.getUint32(16),
+ offset_lsB: view.getUint32(20),
+ length: view.getUint32(24),
+ };
+
+ /* we don't support writes, so nothing needs the data at present */
+ /* req.data = buf.slice(28); */
+
+ var err = 0;
+ var consumed = 28;
+
+ /* the command handlers return 0 on success, and send their
+ * own response. Otherwise, a non-zero error code will be
+ * used as a simple error response
+ */
+ switch (req.type) {
+ case NBD_CMD_READ:
+ err = this._handle_cmd_read(req);
+ break;
+
+ case NBD_CMD_DISC:
+ err = this._handle_cmd_disconnect(req);
+ break;
+
+ case NBD_CMD_WRITE:
+ /* we also need length bytes of data to consume a write
+ * request */
+ if (buf.byteLength < 28 + req.length)
+ return 0;
+ consumed += req.length;
+ err = EPERM;
+ break;
+
+ case NBD_CMD_TRIM:
+ err = EPERM;
+ break;
+
+ default:
+ this._log("invalid command 0x" + req.type.toString(16));
+ err = EINVAL;
+ }
+
+ if (err) {
+ var resp = this._create_cmd_response(req, err);
+ this.ws.send(resp);
+ }
+
+ return consumed;
+ }
+
+ this._handle_cmd_read = function(req)
+ {
+ if (req.offset_msB)
+ return ENOSPC;
+
+ if (req.offset_lsB + req.length > file.size)
+ return ENOSPC;
+
+ this._log("read: 0x" + req.length.toString(16) +
+ " bytes, offset 0x" + req.offset_lsB.toString(16));
+
+ var blob = this.file.slice(req.offset_lsB,
+ req.offset_lsB + req.length);
+ var reader = new FileReader();
+
+ reader.onload = (function(ev) {
+ var reader = ev.target;
+ if (reader.readyState != FileReader.DONE)
+ return;
+ var resp = this._create_cmd_response(req, 0, reader.result);
+ this.ws.send(resp);
+ }).bind(this);
+
+ reader.onerror = (function(ev) {
+ var reader = ev.target;
+ this._log("error reading file: " + reader.error);
+ var resp = this._create_cmd_response(req, EIO);
+ this.ws.send(resp);
+ }).bind(this);
+
+ reader.readAsArrayBuffer(blob);
+
+ return 0;
+ }
+
+ this._handle_cmd_disconnect = function(req)
+ {
+ this._log("disconnect received");
+ this.stop();
+ return 0;
+ }
+
+ this.recv_handlers = Object.freeze({
+ [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this),
+ [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this),
+ [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this),
+ });
+}
+
+