diff options
Diffstat (limited to 'src/plugins/console')
-rw-r--r-- | src/plugins/console/Makefile | 13 | ||||
-rw-r--r-- | src/plugins/console/console.html | 92 | ||||
-rw-r--r-- | src/plugins/console/console.js | 451 | ||||
-rw-r--r-- | src/plugins/console/plugin-console.c | 821 |
4 files changed, 1377 insertions, 0 deletions
diff --git a/src/plugins/console/Makefile b/src/plugins/console/Makefile new file mode 100644 index 0000000..efba399 --- /dev/null +++ b/src/plugins/console/Makefile @@ -0,0 +1,13 @@ +ifneq ($(strip $(MAKECMDGOALS)),) +%: + $(MAKE) -C .. $(MAKECMDGOALS) +else +all: + $(MAKE) -C .. all +endif + +%.crt: + cert="$@"; \ + make -f /etc/ssl/certs/Makefile $@ && \ + mv $${cert%.crt}.key $${cert%.crt}.key.protected && \ + openssl rsa -in $${cert%.crt}.key.protected -out $${cert%.crt}.key diff --git a/src/plugins/console/console.html b/src/plugins/console/console.html new file mode 100644 index 0000000..56c50f7 --- /dev/null +++ b/src/plugins/console/console.html @@ -0,0 +1,92 @@ +<html> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-15"> + +<style type="text/css"> +textarea.console_input { + font-size: 12pt; + font-family: monospace; + background-color: black; + padding: 2px; + margin: 0 0 0 0; + color: white; + resize: none; + overflow: visible; +} + +textarea.console_output { + font-size: 12pt; + font-family: monospace; + background-color: black; + padding: 2px; + margin: 0 0 0 0; + color: white; + resize: none; +} +</style> + +<script type="text/javascript" src="console.js"></script> + +<title>Murphy Console (disconnected)</title> +</head> + + +<body onLoad="setupConsole();"> + +<script type="text/javascript"> + +var mrpc = null; + +function setupConsole () { + var cmds = { + connect: cmd_connect, + disconnect: cmd_disconnect, + resize: cmd_resize + }; + + mrpc = new MrpConsole("console_div", cmds); + var addr = mrpc.socketUri(document.URL); + + console.log("Trying to connect to Murphy @ " + addr); + + mrpc.connect(addr, "murphy"); + mrpc.focus(); + + mrpc.onconnected = function () { + document.title = "Murphy Console @ " + addr; + mrpc.append("Connection to Murphy console established.\n"); + }; + + mrpc.onclosed = function () { + document.title = "Murphy Console (disconnected)"; + mrpc.append("Murphy console connection closed.\n"); + mrpc.append("Use 'connect' to try to reconnect.\n"); + }; +} + + +function cmd_connect(args) { + var addr; + + addr = mrpc.socketUri(document.URL); + console.log("Trying to reconnect..."); + mrpc.connect(addr, "murphy"); +} + + +function cmd_disconnect() { + + console.log("Disconnecting..."); + addr = mrpc.disconnect(); +} + + +function cmd_resize(args) { + mrpc.resize(args[0], args[1]); +} + + +</script> + +<div id="console_div"></div> +</body></html> diff --git a/src/plugins/console/console.js b/src/plugins/console/console.js new file mode 100644 index 0000000..f3b6f4c --- /dev/null +++ b/src/plugins/console/console.js @@ -0,0 +1,451 @@ +/* + * An Ode to My Suckage in Javascript... + * + * It'd be nice if someone wrote a relatively simple readline-like + output + * javascript package (ie. not a full VT100 terminal emulator like termlib). + */ + + +/* + * custom console error type + */ + +function MrpConsoleError(message) { + this.name = "Murphy Console Error"; + this.message = message; +} + + +/** Create a Murphy console, put it after next_elem_id. */ +function MrpConsole(next_elem_id, user_commands) { + var sck, input, output, div, next_elem; + + this.reset(); + this.commands = user_commands; + + /* create output text area */ + output = document.createElement("textarea"); + output.console = this; + this.output = output; + + output.setAttribute("class" , "console_output"); + output.setAttribute("cols" , 80); + output.setAttribute("rows" , 25); + output.setAttribute("readonly" , true); + output.setAttribute("disabled" , true); + output.setAttribute("spellcheck", false); + output.nline = 0; + output.onfocus = function () { return false; } + + /* create input text area, hook up input handler */ + input = document.createElement("textarea"); + input.console = this; + this.input = input; + + input.setAttribute("class" , "console_input"); + input.setAttribute("cols" , 81); + input.setAttribute("rows" , 1); + input.setAttribute("spellcheck", false); + input.setAttribute("autofocus" , "autofocus"); + + input.onkeyup = this.onkeyup; + input.onkeypress = this.onkeypress; + + next_elem = document.getElementById(next_elem_id); + + if (!next_elem) + throw new MrpConsoleError("element " + next_elem_id + " not found"); + + div = document.createElement("div"); + div.appendChild(output); + div.appendChild(input); + + /* insert console div to document */ + document.body.insertBefore(div, next_elem); + + this.setInput(""); +} + + +/** Reset/initialize internal state to the disconnected defaults. */ +MrpConsole.prototype.reset = function () { + this.connected = false; + this.server = null; + this.sck = null; + + this.history = new Array (); + this.histidx = 0; + if (!this.input) + this.prompt = "disconnected"; + else + this.setPrompt("disconnected"); +} + + +/** Resize the console. */ +MrpConsole.prototype.resize = function (width, height) { + if (width && width > 0) { + this.output.cols = width; + this.input.cols = width; + } + if (height && height > 0) + this.output.rows = height; +} + + +/** Get the focus to the console. */ +MrpConsole.prototype.focus = function () { + if (this.input) + this.input.focus(); +} + + +/** Write output to the console, replacing its current contents. */ +MrpConsole.prototype.write = function (text, noscroll) { + var out = this.output; + + out.value = text; + out.nline = text.split("\n").length; + + if (!noscroll) + this.scrollBottom(); +} + + +/** Append output to the console. */ +MrpConsole.prototype.append = function (text, noscroll) { + var out = this.output; + + out.value += text; + out.nline += text.split("\n").length; + + if (!noscroll) + this.scrollBottom(); +} + + +/** Set the content of the input field to 'prompt> text'. */ +MrpConsole.prototype.setInput = function (text) { + this.input.value = this.prompt + "> " + text; +} + + +/** Get the current input value (without the prompt). */ +MrpConsole.prototype.getInput = function () { + if (this.input) + return this.input.value.slice(this.prompt.length + 2).split("\n")[0]; + else + return ""; +} + + +/** Set the input prompt to the given value. */ +MrpConsole.prototype.setPrompt = function (prompt) { + var value = this.getInput(); + + if (!this.input) + this.prompt = prompt; + else { + this.prompt = prompt; + this.input.value = this.prompt + "> " + value; + } +} + + +/** Scroll the output window up or down the given amount of lines. */ +MrpConsole.prototype.scroll = function (amount) { + var out = this.output; + var pxl = (out.nline ? (out.scrollHeight / out.nline) : 0); + var top = out.scrollTop + (amount * pxl); + + if (top < 0) + top = 0; + if (top > out.scrollHeight) + top = out.scrollHeight; + + out.scrollTop = top; +} + + +/** Scroll the output window up or down by a 'page'. */ +MrpConsole.prototype.scrollPage = function (dir) { + var out = this.output; + var nline = 2 * 25 / 3; + + if (dir < 0) + dir = -1; + else + dir = +1; + + this.scroll(dir * nline); +} + + +/** Scroll to the bottom. */ +MrpConsole.prototype.scrollBottom = function () { + this.output.scrollTop = this.output.scrollHeight; +} + + +/** Add a new entry to the history. */ +MrpConsole.prototype.historyAppend = function (entry) { + if (entry.length > 0) { + this.history.push(entry); + this.histidx = this.history.length; + + this.setInput(""); + } +} + + +/** Go to the previous history entry. */ +MrpConsole.prototype.historyShow = function (dir) { + var idx = this.histidx + dir; + + if (0 <= idx && idx < this.history.length) { + if (this.histidx == this.history.length && + this.input.value.length > 0) { + /* Hmm... autoinsert to history, not the Right Thing To Do... */ + this.historyAppend(this.getInput()); + } + + this.histidx = idx; + this.setInput(this.history[this.histidx]); + } + else if (idx >= this.history.length) { + this.histidx = this.history.length; + this.setInput(""); + } +} + + +/** Make sure the input position never enters the prompt. */ +MrpConsole.prototype.checkInputPosition = function () { + var pos = this.input.selectionStart; + + if (pos <= this.prompt.length + 2) { + this.input.selectionStart = this.prompt.length + 2; + this.input.selectionEnd = this.prompt.length + 2; + return false; + } + else + return true; +} + + +/** Key up handler. */ +MrpConsole.prototype.onkeyup = function (e) { + var c = this.console; + var l; + + /*console.log("got key " + e.which);*/ + + switch (e.keyCode) { + case e.DOM_VK_RETURN: + if (c.input.value.length > c.prompt.length + 2) { + l = c.getInput(); + c.historyAppend(l); + c.processCmd(l); + c.setInput(""); + } + break; + case e.DOM_VK_PAGE_UP: + if (e.shiftKey) + c.scrollPage(-1); + break; + case e.DOM_VK_PAGE_DOWN: + if (e.shiftKey) + c.scrollPage(+1); + break; + + case e.DOM_VK_UP: + if (!e.shiftKey) + c.historyShow(-1); + else + c.scroll(-1); + break; + case e.DOM_VK_DOWN: + if (!e.shiftKey) + c.historyShow(+1); + else + c.scroll(+1); + break; + case e.DOM_VK_LEFT: + case e.DOM_VK_BACK_SPACE: + if (!c.checkInputPosition()) + return false; + break; + } + + return true; +} + + +/** Key-press handler. */ +MrpConsole.prototype.onkeypress = function (e) { + var c = this.console; + var rows; + + switch (e.which) { + case e.DOM_VK_LEFT: + case e.DOM_VK_BACK_SPACE: + if (!c.checkInputPosition()) + return false; + } + + rows = Math.floor(1 + (c.input.value.length / c.input.cols)); + + if (c.input.rows < rows) + c.input.rows = rows; + else if (c.input.rows > rows) + c.input.rows = rows; + + return true; +} + + +/** Connect to the Murphy daemon running at the given address. */ +MrpConsole.prototype.connect = function (address) { + var c = this.console; + + if (this.connected) + throw new MrpConsoleError("already connected to " + this.address); + else { + this.server = address; + this.connected = false; + + this.setPrompt("connecting"); + + if (typeof MozWebSocket != "undefined") + sck = new MozWebSocket(this.server, "murphy"); + else + sck = new WebSocket(this.server, "murphy"); + + this.sck = sck; + sck.console = this; + sck.onopen = this.sckconnect; + sck.onclose = this.sckclosed; + sck.onerror = this.sckerror; + sck.onmessage = this.sckmessage; + } +} + + +/** Close the console connection. */ +MrpConsole.prototype.disconnect = function () { + if (this.connected) { + this.sck.close(); + } +} + + +/** Connection established event handler. */ +MrpConsole.prototype.sckconnect = function () { + var c = this.console; + + c.connected = true; + c.setPrompt("connected"); + + if (c.onconnected) + c.onconnected(); +} + + +/** Connection shutdown event handler. */ +MrpConsole.prototype.sckclosed = function () { + var c = this.console; + + console.log("socket closed"); + + c.reset(); + + if (c.onclosed) + c.onclosed(); +} + + +/** Socket error event handler. */ +MrpConsole.prototype.sckerror = function (e) { + var c = this.console; + + c.reset(); + + if (c.onerror) + c.onerror(); + + if (c.onclosed) + c.onclosed(); +} + + +/** Socket message event handler. */ +MrpConsole.prototype.sckmessage = function (message) { + var c = this.console; + var msg = JSON.parse(message.data); + + if (msg.prompt) + c.setPrompt(msg.prompt); + else { + if (msg.output) { + c.append(msg.output); + } + } +} + + +/** Send a request to the server. */ +MrpConsole.prototype.send_request = function (req) { + var sck = this.sck; + + sck.send(JSON.stringify(req)); +} + + +/** Process a command entered by the user. */ +MrpConsole.prototype.processCmd = function (cmd) { + var c, l, cb, args; + + for (c in this.commands) { + l = c.length; + cb = this.commands[c]; + if (cmd.substring(0, l) == c && (cmd.length == l || + cmd.substring(l, l + 1) == ' ')) { + args = cmd.split(' ').splice(1); + + this.commands[c](args); + + return; + } + } + + this.append(this.prompt + "> " + cmd + "\n"); + this.send_request({ input: cmd }); + + if (cmd == 'help') { + if (this.commands && Object.keys(this.commands).length > 0) { + this.append("Web console commands:\n"); + for (c in this.commands) { + this.append(" " + c + "\n"); + } + } + else + this.append("No Web console commands."); + } +} + + +/** Determine a WebSocket URI based on an HTTP URI. */ +MrpConsole.prototype.socketUri = function (http_uri) { + var proto, colon, rest; + + colon = http_uri.indexOf(':'); /* get first colon */ + proto = http_uri.substring(0, colon); /* get protocol */ + rest = http_uri.substring(colon + 3); /* get URI sans protocol:// */ + addr = rest.split("/")[0]; /* strip URI path from address */ + + switch (proto) { + case "http": return "ws://" + addr; + case "https": return "wss://" + addr; + default: return null; + } +} diff --git a/src/plugins/console/plugin-console.c b/src/plugins/console/plugin-console.c new file mode 100644 index 0000000..13fa207 --- /dev/null +++ b/src/plugins/console/plugin-console.c @@ -0,0 +1,821 @@ +/* + * Copyright (c) 2012, Intel Corporation + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Intel Corporation nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <stdarg.h> +#include <netdb.h> +#include <sys/types.h> +#include <sys/socket.h> + +#include <murphy/common.h> +#include <murphy/core.h> + +#include "config.h" + +#ifdef WEBSOCKETS_ENABLED +# include <murphy/common/wsck-transport.h> +# include <murphy/common/json.h> +#endif + +#include <murphy/plugins/console-protocol.h> + +#define DEFAULT_ADDRESS "unxs:@murphy-console" /* default console address */ + +#ifdef MURPHY_DATADIR /* default content dir */ +# define DEFAULT_HTTPDIR MURPHY_DATADIR"/webconsole" +#else +# define DEFAULT_HTTPDIR "/usr/share/murphy/webconsole" +#endif + +enum { + DEBUG_NONE = 0x0, + DEBUG_FUNC = 0x1, + DEBUG_FILE = 0x2, + DEBUG_LINE = 0x4, + DEBUG_DEFAULT = DEBUG_FUNC +}; + + +/* + * an active console instance + */ + +typedef struct { + mrp_console_t *mc; /* associated murphy console */ + mrp_transport_t *t; /* associated transport */ + mrp_sockaddr_t addr; /* for temp. datagram 'connection' */ + socklen_t alen; /* address length if any */ + int id; /* console ID for log redirection */ + int dbgmeta; /* debug metadata to show */ +} console_t; + + +/* + * console plugin data + */ + +typedef struct { + const char *address; /* console address */ + mrp_transport_t *t; /* transport we're listening on */ + mrp_context_t *ctx; /* murphy context */ + mrp_list_hook_t clients; /* active console clients */ + mrp_sockaddr_t addr; /* resolved transport address */ + socklen_t alen; /* address length */ + console_t *c; /* datagram console being served */ + const char *httpdir; /* WRT console agent directory */ + const char *sslcert; /* path to SSL certificate */ + const char *sslpkey; /* path to SSL private key */ + const char *sslca; /* path to SSL CA */ +} data_t; + + + +static int next_id = 1; + +static ssize_t write_req(mrp_console_t *mc, void *buf, size_t size) +{ + console_t *c = (console_t *)mc->backend_data; + mrp_msg_t *msg; + uint16_t tag, type; + uint32_t len; + + tag = MRP_CONSOLE_OUTPUT; + type = MRP_MSG_FIELD_BLOB; + len = size; + msg = mrp_msg_create(tag, type, len, buf, NULL); + + if (msg != NULL) { + mrp_transport_send(c->t, msg); + mrp_msg_unref(msg); + + return size; + } + else + return -1; +} + + +static void logger(void *data, mrp_log_level_t level, const char *file, + int line, const char *func, const char *format, va_list ap) +{ + console_t *c = (console_t *)data; + va_list cp; + const char *prefix; + char buf[256], lnstr[64]; + + MRP_UNUSED(file); + MRP_UNUSED(line); + MRP_UNUSED(func); + + switch (level) { + case MRP_LOG_ERROR: prefix = "[log] E: "; break; + case MRP_LOG_WARNING: prefix = "[log] W: "; break; + case MRP_LOG_INFO: prefix = "[log] I: "; break; + case MRP_LOG_DEBUG: + if (c->dbgmeta & DEBUG_LINE) + snprintf(lnstr, sizeof(lnstr), ":%d", line); + else + lnstr[0] = '\0'; + snprintf(buf, sizeof(buf), "[log] D: %s%s%s%s%s%s%s", + c->dbgmeta ? "[" : "", + c->dbgmeta & DEBUG_FUNC ? func : "", + c->dbgmeta & DEBUG_FILE ? "@" : "", + c->dbgmeta & DEBUG_FILE ? file : "", + c->dbgmeta & DEBUG_LINE ? lnstr : "", + c->dbgmeta ? "]" : "", + c->dbgmeta ? " " : ""); + prefix = buf; + break; + default: prefix = "[log] ?: "; + } + + va_copy(cp, ap); + mrp_console_printf(c->mc, "%s", prefix); + mrp_console_vprintf(c->mc, format, cp); + mrp_console_printf(c->mc, "\n"); + va_end(cp); +} + + +static void debug_cb(mrp_console_t *mc, void *user_data, int argc, char **argv) +{ + console_t *c = (console_t *)mc->backend_data; + int debug; + const char *p, *n; + int i, l; + + MRP_UNUSED(user_data); + + debug = 0; + for (i = 2; i < argc; i++) { + p = argv[i]; + while (p && *p) { + if ((n = strchr(p, ',')) != NULL) + l = n - p; + else + l = strlen(p); + + if (!strncmp(p, "function", l) || + !strncmp(p, "func" , l)) debug |= DEBUG_FUNC; + else if (!strncmp(p, "file" , l)) debug |= DEBUG_FILE; + else if (!strncmp(p, "line" , l)) debug |= DEBUG_LINE; + else + mrp_log_warning("Unknown console debug flag '%*.*s'.", l, l, p); + + if ((p = n) != NULL) + p++; + } + } + + c->dbgmeta = debug & ((debug & ~DEBUG_LINE) ? -1 : ~DEBUG_LINE); + + if (c->dbgmeta != debug) + mrp_log_warning("Orphan console debug flag 'line' forced off."); +} + + +static void register_logger(console_t *c) +{ + char name[32]; + + if (!c->id) + return; + + snprintf(name, sizeof(name), "console/%d", c->id); + mrp_log_register_target(name, logger, c); +} + + +static void unregister_logger(console_t *c) +{ + char name[32]; + + if (!c->id) + return; + + snprintf(name, sizeof(name), "console/%d", c->id); + mrp_log_unregister_target(name); +} + + +static void set_prompt_req(mrp_console_t *mc, const char *prompt) +{ + console_t *c = (console_t *)mc->backend_data; + mrp_msg_t *msg; + uint16_t tag, type; + + tag = MRP_CONSOLE_PROMPT; + type = MRP_MSG_FIELD_STRING; + msg = mrp_msg_create(tag, type, prompt, NULL); + + if (msg != NULL) { + mrp_transport_send(c->t, msg); + mrp_msg_unref(msg); + } +} + + +static void free_req(void *backend_data) +{ + mrp_free(backend_data); +} + + +static void recv_cb(mrp_transport_t *t, mrp_msg_t *msg, void *user_data) +{ + console_t *c = (console_t *)user_data; + mrp_msg_field_t *f; + char *input; + size_t size; + + MRP_UNUSED(t); + + if ((f = mrp_msg_find(msg, MRP_CONSOLE_INPUT)) != NULL) { + if (f->type == MRP_MSG_FIELD_BLOB) { + input = f->str; + size = f->size[0]; + + if (size > 0) { + MRP_CONSOLE_BUSY(c->mc, { + c->mc->evt.input(c->mc, input, size); + }); + + c->mc->check_destroy(c->mc); + } + + return; + } + } + + mrp_log_warning("Ignoring malformed message from console/%d...", c->id); +} + + +static void recvfrom_cb(mrp_transport_t *t, mrp_msg_t *msg, + mrp_sockaddr_t *addr, socklen_t alen, void *user_data) +{ + console_t *c = (console_t *)user_data; + mrp_sockaddr_t obuf; + socklen_t olen; + mrp_msg_field_t *f; + char *input; + size_t size; + + MRP_UNUSED(t); + + if ((f = mrp_msg_find(msg, MRP_CONSOLE_INPUT)) != NULL) { + if (f->type == MRP_MSG_FIELD_BLOB) { + input = f->str; + size = f->size[0]; + + if (size > 0) { + mrp_sockaddr_cpy(&obuf, &c->addr, olen = c->alen); + mrp_sockaddr_cpy(&c->addr, addr, c->alen = alen); + mrp_transport_connect(t, addr, alen); + + MRP_CONSOLE_BUSY(c->mc, { + c->mc->evt.input(c->mc, input, size); + }); + + c->mc->check_destroy(c->mc); + + + mrp_transport_disconnect(t); + + if (olen) { + mrp_transport_connect(t, &obuf, olen); + mrp_sockaddr_cpy(&c->addr, &obuf, c->alen = olen); + } + + return; + } + } + } + + mrp_log_warning("Ignoring malformed message from console/%d...", c->id); +} + + +/* + * generic stream transport + */ + +#define stream_write_req write_req +#define stream_set_prompt_req set_prompt_req +#define stream_free_req free_req +#define stream_recv_cb recv_cb + +static void stream_close_req(mrp_console_t *mc) +{ + console_t *c = (console_t *)mc->backend_data; + + if (c->t != NULL) { + mrp_transport_disconnect(c->t); + mrp_transport_destroy(c->t); + unregister_logger(c); + + c->t = NULL; + } +} + + +static void stream_connection_cb(mrp_transport_t *lt, void *user_data) +{ + static mrp_console_req_t req; + + data_t *data = (data_t *)user_data; + console_t *c; + int flags; + + if ((c = mrp_allocz(sizeof(*c))) != NULL) { + flags = MRP_TRANSPORT_REUSEADDR | MRP_TRANSPORT_NONBLOCK; + c->t = mrp_transport_accept(lt, c, flags); + + if (c->t != NULL) { + req.write = stream_write_req; + req.close = stream_close_req; + req.free = stream_free_req; + req.set_prompt = stream_set_prompt_req; + + c->mc = mrp_create_console(data->ctx, &req, c); + + if (c->mc != NULL) { + c->id = next_id++; + c->dbgmeta = DEBUG_DEFAULT; + register_logger(c); + + return; + } + else { + mrp_transport_destroy(c->t); + c->t = NULL; + } + } + } +} + + +static void stream_closed_cb(mrp_transport_t *t, int error, void *user_data) +{ + console_t *c = (console_t *)user_data; + + if (error) + mrp_log_error("Connection to console/%d closed with error %d (%s).", + c->id, error, strerror(error)); + else { + mrp_log_info("console/%d has closed the connection.", c->id); + + mrp_transport_disconnect(t); + mrp_transport_destroy(t); + unregister_logger(c); + c->t = NULL; + } +} + + +static int stream_setup(data_t *data) +{ + static mrp_transport_evt_t evt; + + mrp_mainloop_t *ml = data->ctx->ml; + mrp_transport_t *t; + const char *type; + mrp_sockaddr_t addr; + socklen_t alen; + int flags; + + t = NULL; + alen = sizeof(addr); + alen = mrp_transport_resolve(NULL, data->address, &addr, alen, &type); + + if (alen <= 0) { + mrp_log_error("Failed to resolve console transport address '%s'.", + data->address); + + return FALSE; + } + + evt.connection = stream_connection_cb; + evt.closed = stream_closed_cb; + evt.recvmsg = stream_recv_cb; + evt.recvmsgfrom = NULL; + + flags = MRP_TRANSPORT_REUSEADDR; + t = mrp_transport_create(ml, type, &evt, data, flags); + + if (t != NULL) { + if (mrp_transport_bind(t, &addr, alen) && mrp_transport_listen(t, 1)) { + data->t = t; + + return TRUE; + } + else { + mrp_log_error("Failed to bind console to '%s'.", data->address); + mrp_transport_destroy(t); + } + } + else + mrp_log_error("Failed to create console transport."); + + return FALSE; +} + + +/* + * datagram transports + */ + +#define dgram_write_req write_req +#define dgram_free_req free_req +#define dgram_set_prompt_req set_prompt_req + +#define dgram_recv_cb recv_cb +#define dgram_recvfrom_cb recvfrom_cb + + +static void dgram_close_req(mrp_console_t *mc) +{ + console_t *c = (console_t *)mc->backend_data; + mrp_msg_t *msg; + uint16_t tag, type; + + tag = MRP_CONSOLE_BYE; + type = MRP_MSG_FIELD_BOOL; + msg = mrp_msg_create(tag, type, TRUE, NULL); + + if (msg != NULL) { + mrp_transport_send(c->t, msg); + mrp_msg_unref(msg); + } + + mrp_transport_disconnect(c->t); +} + + +static int dgram_setup(data_t *data) +{ + static mrp_transport_evt_t evt; + static mrp_console_req_t req; + + mrp_mainloop_t *ml = data->ctx->ml; + mrp_transport_t *t; + const char *type; + mrp_sockaddr_t addr; + socklen_t alen; + int flags; + console_t *c; + + t = NULL; + alen = sizeof(addr); + alen = mrp_transport_resolve(NULL, data->address, &addr, alen, &type); + + if (alen <= 0) { + mrp_log_error("Failed to resolve console transport address '%s'.", + data->address); + + return FALSE; + } + + c = mrp_allocz(sizeof(*c)); + + if (c != NULL) { + evt.recvmsg = dgram_recv_cb; + evt.recvmsgfrom = dgram_recvfrom_cb; + evt.connection = NULL; + evt.closed = NULL; + + flags = MRP_TRANSPORT_REUSEADDR; + t = mrp_transport_create(ml, type, &evt, c, flags); + + if (t != NULL) { + if (mrp_transport_bind(t, &addr, alen)) { + req.write = dgram_write_req; + req.close = dgram_close_req; + req.free = dgram_free_req; + req.set_prompt = dgram_set_prompt_req; + + c->t = t; + c->mc = mrp_create_console(data->ctx, &req, c); + + if (c->mc != NULL) { + data->c = c; + c->mc->preserve = TRUE; + + return TRUE; + } + else + mrp_log_error("Failed to create console."); + } + else + mrp_log_error("Failed to bind console to '%s'.", data->address); + + c->t = NULL; + mrp_transport_destroy(t); + } + else + mrp_log_error("Failed to create console transport."); + + mrp_free(c); + } + + return FALSE; +} + + +#ifdef WEBSOCKETS_ENABLED + +/* + * websocket transport + */ + +#define wsock_close_req stream_close_req +#define wsock_free_req free_req +#define wsock_closed_cb stream_closed_cb + +static ssize_t wsock_write_req(mrp_console_t *mc, void *buf, size_t size) +{ + console_t *c = (console_t *)mc->backend_data; + mrp_json_t *msg; + + msg = mrp_json_create(MRP_JSON_OBJECT); + + if (msg != NULL) { + if (mrp_json_add_string_slice(msg, "output", buf, size)) + mrp_transport_sendcustom(c->t, msg); + + mrp_json_unref(msg); + + return size; + } + else + return -1; +} + + +static void wsock_set_prompt_req(mrp_console_t *mc, const char *prompt) +{ + console_t *c = (console_t *)mc->backend_data; + mrp_json_t *msg; + + msg = mrp_json_create(MRP_JSON_OBJECT); + + if (msg != NULL) { + if (mrp_json_add_string(msg, "prompt", prompt)) + mrp_transport_sendcustom(c->t, msg); + + mrp_json_unref(msg); + } +} + + +static void wsock_recv_cb(mrp_transport_t *t, void *data, void *user_data) +{ + console_t *c = (console_t *)user_data; + mrp_json_t *msg = (mrp_json_t *)data; + const char *s; + char *input; + size_t size; + + MRP_UNUSED(t); + + s = mrp_json_object_to_string((mrp_json_t *)data); + + mrp_debug("recived WRT console message:"); + mrp_debug(" %s", s); + + if (mrp_json_get_string(msg, "input", &input)) { + size = strlen(input); + + if (size > 0) { + MRP_CONSOLE_BUSY(c->mc, { + c->mc->evt.input(c->mc, input, size); + }); + + c->mc->check_destroy(c->mc); + } + } +} + + +static void wsock_connection_cb(mrp_transport_t *lt, void *user_data) +{ + static mrp_console_req_t req; + data_t *data = (data_t *)user_data; + console_t *c; + + mrp_debug("incoming web console connection..."); + + if ((c = mrp_allocz(sizeof(*c))) != NULL) { + c->t = mrp_transport_accept(lt, c, 0); + + if (c->t != NULL) { + req.write = wsock_write_req; + req.close = wsock_close_req; + req.free = wsock_free_req; + req.set_prompt = wsock_set_prompt_req; + + c->mc = mrp_create_console(data->ctx, &req, c); + + if (c->mc != NULL) { + c->id = next_id++; + register_logger(c); + + return; + } + else { + mrp_transport_destroy(c->t); + c->t = NULL; + } + } + } +} + + +static int wsock_setup(data_t *data) +{ + static mrp_transport_evt_t evt; + + mrp_mainloop_t *ml = data->ctx->ml; + const char *cert = data->sslcert; + const char *pkey = data->sslpkey; + const char *ca = data->sslca; + mrp_transport_t *t; + const char *type; + mrp_sockaddr_t addr; + socklen_t alen; + int flags; + + t = NULL; + alen = sizeof(addr); + alen = mrp_transport_resolve(NULL, data->address, &addr, alen, &type); + + if (alen <= 0) { + mrp_log_error("Failed to resolve console transport address '%s'.", + data->address); + + return FALSE; + } + + evt.connection = wsock_connection_cb; + evt.closed = wsock_closed_cb; + evt.recvcustom = wsock_recv_cb; + evt.recvmsgfrom = NULL; + + flags = MRP_TRANSPORT_MODE_CUSTOM; + t = mrp_transport_create(ml, type, &evt, data, flags); + + if (t != NULL) { + if (cert || pkey || ca) { + mrp_transport_setopt(t, MRP_WSCK_OPT_SSL_CERT, cert); + mrp_transport_setopt(t, MRP_WSCK_OPT_SSL_PKEY, pkey); + mrp_transport_setopt(t, MRP_WSCK_OPT_SSL_CA , ca); + } + + if (mrp_transport_bind(t, &addr, alen) && mrp_transport_listen(t, 1)) { + mrp_transport_setopt(t, MRP_WSCK_OPT_HTTPDIR, data->httpdir); + data->t = t; + + return TRUE; + } + else { + mrp_log_error("Failed to bind console to '%s'.", data->address); + mrp_transport_destroy(t); + } + } + else + mrp_log_error("Failed to create console transport."); + + return FALSE; +} + +#endif /* WEBSOCKETS_ENABLED */ + + +enum { + ARG_ADDRESS, /* console transport address */ + ARG_HTTPDIR, /* content directory for HTTP */ + ARG_SSLCERT, /* path to SSL certificate */ + ARG_SSLPKEY, /* path to SSL private key */ + ARG_SSLCA /* path to SSL CA */ +}; + + + +static int console_init(mrp_plugin_t *plugin) +{ + data_t *data; + int ok; + + if ((data = mrp_allocz(sizeof(*data))) != NULL) { + mrp_list_init(&data->clients); + + data->ctx = plugin->ctx; + data->address = plugin->args[ARG_ADDRESS].str; + data->httpdir = plugin->args[ARG_HTTPDIR].str; + data->sslcert = plugin->args[ARG_SSLCERT].str; + data->sslpkey = plugin->args[ARG_SSLPKEY].str; + data->sslca = plugin->args[ARG_SSLCA].str; + + mrp_log_info("Using console address '%s'...", data->address); + + if (!strncmp(data->address, "wsck:", 5)) { + if (data->httpdir != NULL) + mrp_log_info("Using '%s' for serving console Web agent...", + data->httpdir); + else + mrp_log_info("Not serving console Web agent..."); + } + + if (!strncmp(data->address, "tcp4:", 5) || + !strncmp(data->address, "tcp6:", 5) || + !strncmp(data->address, "unxs:", 5)) + ok = stream_setup(data); +#ifdef WEBSOCKETS_ENABLED + else if (!strncmp(data->address, "wsck:", 5)) + ok = wsock_setup(data); +#endif + else + ok = dgram_setup(data); + + if (ok) { + plugin->data = data; + + return TRUE; + } + } + + mrp_free(data); + + return FALSE; +} + + +static void console_exit(mrp_plugin_t *plugin) +{ + mrp_log_info("Cleaning up %s...", plugin->instance); +} + + +#define CONSOLE_DESCRIPTION "A debug console for Murphy." +#define CONSOLE_HELP \ + "The debug console provides a telnet-like remote session and a\n" \ + "simple shell-like command interpreter with commands to help\n" \ + "development, debugging, and trouble-shooting. The set of commands\n" \ + "can be dynamically extended by registering new commands from\n" \ + "other plugins." + +#define CONSOLE_VERSION MRP_VERSION_INT(0, 0, 1) +#define CONSOLE_AUTHORS "Krisztian Litkey <kli@iki.fi>" + + +static mrp_plugin_arg_t console_args[] = { + MRP_PLUGIN_ARGIDX(ARG_ADDRESS , STRING, "address", DEFAULT_ADDRESS), + MRP_PLUGIN_ARGIDX(ARG_HTTPDIR , STRING, "httpdir", DEFAULT_HTTPDIR), + MRP_PLUGIN_ARGIDX(ARG_SSLCERT , STRING, "sslcert", NULL), + MRP_PLUGIN_ARGIDX(ARG_SSLPKEY , STRING, "sslpkey", NULL), + MRP_PLUGIN_ARGIDX(ARG_SSLCA , STRING, "sslca" , NULL) +}; + + +MRP_CONSOLE_GROUP(console_commands, "console", NULL, NULL, { + MRP_TOKENIZED_CMD("debug", debug_cb, FALSE, + "debug [function] [file] [line]", + "set debug metadata to show", + "Set what metadata to show for debug messages."), +}); + +MURPHY_REGISTER_CORE_PLUGIN("console", + CONSOLE_VERSION, CONSOLE_DESCRIPTION, + CONSOLE_AUTHORS, CONSOLE_HELP, MRP_MULTIPLE, + console_init, console_exit, + console_args, MRP_ARRAY_SIZE(console_args), + NULL, 0, NULL, 0, &console_commands); |