summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Luis Millán <jmillan@aliax.net>2018-01-29 15:21:39 +0100
committerAlfred E. Heggestad <alfred.heggestad@gmail.com>2018-01-29 15:21:39 +0100
commit376796ffe68fb078c77ee7da435acb23fa6a3b3d (patch)
treecddd039aacb9238d9abc949836fa1364321592ae
parentdce37b83eaa6fd543664128bd75f680b418f0d41 (diff)
ctrl_tcp module (#346)
* ctrl_tcp module * ua: set a default reason for UA_EVENT_CALL_CLOSED on user hangup * ctrl_tcp: netstring * ctrl_tcp: fix typo * ctrl_tcp: avoid compiler warnings * ctrl_tcp: fix memory leak * ctrl_tcp: make linter happy * ctrl_tcp: make linter happy * ctrl_tcp: enhance message definition * ctrl_tcp: work on command error and empty response * ctrl_tcp: add response message attribute 'ok' * ctrl_tcp: cleanup * ctrl_tcp: linter. Fix too wide line * ctrl_tcp: Fix. remove unused variable * ctrl_tcp: rename 'netstring-c' folder by 'netstring' * ctrl_tcp: rename 'netstring.[ch]' by 'tcp_netstring.[ch]' * ctrl_tcp: reformat 'netstring' code * ctrl_tcp: move 'netstring_error_str' function to 'netstring' code * ctrl_tcp: remove cstring README file * ctrl_tcp: cleanup cstring makefile * ctrl_tcp: remove makefile and testsuite files from 'netstring' * ctrl_tcp: move variable decalaration to the beginning of the function * ctrl_tcp: add copyright headers
-rw-r--r--mk/modules.mk1
-rw-r--r--modules/ctrl_tcp/ctrl_tcp.c377
-rw-r--r--modules/ctrl_tcp/module.mk10
-rw-r--r--modules/ctrl_tcp/netstring/netstring.c163
-rw-r--r--modules/ctrl_tcp/netstring/netstring.h28
-rw-r--r--modules/ctrl_tcp/tcp_netstring.c221
-rw-r--r--modules/ctrl_tcp/tcp_netstring.h16
-rw-r--r--src/ua.c3
8 files changed, 818 insertions, 1 deletions
diff --git a/mk/modules.mk b/mk/modules.mk
index 24c4b14..6d6ba6e 100644
--- a/mk/modules.mk
+++ b/mk/modules.mk
@@ -275,6 +275,7 @@ MODULES += menu contact vumeter mwi account natpmp httpd
MODULES += srtp
MODULES += uuid
MODULES += debug_cmd
+MODULES += ctrl_tcp
ifneq ($(HAVE_LIBMQTT),)
MODULES += mqtt
diff --git a/modules/ctrl_tcp/ctrl_tcp.c b/modules/ctrl_tcp/ctrl_tcp.c
new file mode 100644
index 0000000..2d0bfcd
--- /dev/null
+++ b/modules/ctrl_tcp/ctrl_tcp.c
@@ -0,0 +1,377 @@
+/**
+ * @file ctrl_tcp.c TCP control interface using JSON payload
+ *
+ * Copyright (C) 2018 46 Labs LLC
+ */
+
+#include <re.h>
+#include <baresip.h>
+
+#include "tcp_netstring.h"
+
+
+/**
+ * @defgroup ctrl_tcp ctrl_tcp
+ *
+ * Communication channel to control and monitor Baresip via JSON messages.
+ *
+ * It receives commands to be executed, sends back command responses and
+ * notifies about events.
+ *
+ * Command message parameters:
+ *
+ * - command : Command to be executed.
+ * - params : Command parameters.
+ * - token : Optional. Included in the response if present.
+ *
+ * Command message example:
+ *
+ \verbatim
+ {
+ "command" : "dial",
+ "params" : "sip:alice@atlanta.com",
+ "token" : "qwerasdf"
+ }
+ \endverbatim
+ *
+ *
+ * Response message parameters:
+ *
+ * - response : true. Identifies the message type.
+ * - ok: : true/false. Indicates whether the command execution succeeded.
+ * - data : Baresip response to the related command execution.
+ * - token : Present if it was included in the related command request.
+ *
+ * Response message example:
+ *
+ \verbatim
+ {
+ "response" : true,
+ "ok" : true,
+ "data" : "",
+ "token" : "qwerasdf"
+ }
+ \endverbatim
+ *
+ *
+ * Event message parameters:
+ *
+ * - event : true. Identifies the message type.
+ * - class : Event class.
+ * - type : Event ID.
+ * - param : Specific event information.
+ *
+ * Apart from the above, events may contain aditional parameters.
+ *
+ * Event message example:
+ *
+ \verbatim
+ {
+ "event" : "true",
+ "class" : "call",
+ "type" : "CALL_CLOSED",
+ "param" : "Connection reset by peer",
+ "account" : "sip:alice@atlanta.com",
+ "direction" : "incoming",
+ "peer" : "sip:bob@biloxy.com",
+ "id" : "73a12546589651f8"
+ }
+ \endverbatim
+ *
+ *
+ * Sample config:
+ *
+ \verbatim
+ ctrl_tcp_listen 0.0.0.0:4444 # IP-address and port to listen on
+ \endverbatim
+ */
+
+
+enum {CTRL_PORT = 4444};
+
+struct ctrl_st {
+ struct tcp_sock *ts;
+ struct tcp_conn *tc;
+ struct netstring *ns;
+};
+
+static struct ctrl_st *ctrl = NULL; /* allow only one instance */
+
+static int print_handler(const char *p, size_t size, void *arg)
+{
+ struct mbuf *mb = arg;
+
+ return mbuf_write_mem(mb, (uint8_t *)p, size);
+}
+
+
+static int encode_response(int cmd_error, struct mbuf *resp, const char *token)
+{
+ struct re_printf pf = {print_handler, resp};
+ struct odict *od = NULL;
+ char *buf = NULL;
+ char m[256];
+ int err;
+
+ /* Empty response. */
+ if (resp->pos == NETSTRING_HEADER_SIZE)
+ {
+ buf = mem_alloc(1, NULL);
+ buf[0] = '\0';
+ }
+ else
+ {
+ resp->pos = NETSTRING_HEADER_SIZE;
+ err = mbuf_strdup(resp, &buf,
+ resp->end - NETSTRING_HEADER_SIZE);
+ if (err)
+ return err;
+ }
+
+ err = odict_alloc(&od, 8);
+ if (err)
+ return err;
+
+ err |= odict_entry_add(od, "response", ODICT_BOOL, true);
+ err |= odict_entry_add(od, "ok", ODICT_BOOL, (bool)!cmd_error);
+
+ if (cmd_error && str_len(buf) == 0)
+ err |= odict_entry_add(od, "data", ODICT_STRING,
+ str_error(cmd_error, m, sizeof(m)));
+ else
+ err |= odict_entry_add(od, "data", ODICT_STRING, buf);
+
+ if (token)
+ err |= odict_entry_add(od, "token", ODICT_STRING, token);
+
+ if (err)
+ goto out;
+
+ mbuf_reset(resp);
+ mbuf_init(resp);
+ resp->pos = NETSTRING_HEADER_SIZE;
+
+ err = json_encode_odict(&pf, od);
+ if (err)
+ warning("ctrl_tcp: failed to encode response JSON (%m)\n",
+ err);
+
+ out:
+ mem_deref(buf);
+ mem_deref(od);
+
+ return err;
+}
+
+
+static bool command_handler(struct mbuf *mb, void *arg)
+{
+ struct ctrl_st *st = arg;
+ struct mbuf *resp = mbuf_alloc(2048);
+ struct re_printf pf = {print_handler, resp};
+ struct odict *od = NULL;
+ const struct odict_entry *oe_cmd, *oe_prm, *oe_tok;
+ char buf[256];
+ int err;
+
+ err = json_decode_odict(&od, 32, (const char*)mb->buf, mb->end, 16);
+ if (err) {
+ warning("ctrl_tcp: failed to decode JSON (%m)\n", err);
+ goto out;
+ }
+
+ oe_cmd = odict_lookup(od, "command");
+ oe_prm = odict_lookup(od, "params");
+ oe_tok = odict_lookup(od, "token");
+ if (!oe_cmd) {
+ warning("ctrl_tcp: missing json entries\n");
+ goto out;
+ }
+
+ debug("ctrl_tcp: handle_command: cmd='%s', params:'%s', token='%s'\n",
+ oe_cmd ? oe_cmd->u.str : "",
+ oe_prm ? oe_prm->u.str : "",
+ oe_tok ? oe_tok->u.str : "");
+
+ re_snprintf(buf, sizeof(buf), "%s%s%s",
+ oe_cmd->u.str,
+ oe_prm ? " " : "",
+ oe_prm ? oe_prm->u.str : "");
+
+ resp->pos = NETSTRING_HEADER_SIZE;
+
+ /* Relay message to long commands */
+ err = cmd_process_long(baresip_commands(),
+ buf,
+ str_len(buf),
+ &pf, NULL);
+ if (err) {
+ warning("ctrl_tcp: error processing command (%m)\n", err);
+ }
+
+ err = encode_response(err, resp, oe_tok ? oe_tok->u.str : NULL);
+ if (err) {
+ warning("ctrl_tcp: failed to encode response (%m)\n", err);
+ goto out;
+ }
+
+ resp->pos = NETSTRING_HEADER_SIZE;
+ err = tcp_send(st->tc, resp);
+ if (err) {
+ warning("ctrl_tcp: failed to send the message (%m)\n", err);
+ }
+
+ out:
+ mem_deref(resp);
+ mem_deref(od);
+
+ return true; /* always handled */
+}
+
+
+static void tcp_close_handler(int err, void *arg)
+{
+ struct ctrl_st *st = arg;
+
+ (void)err;
+
+ st->tc = mem_deref(st->tc);
+}
+
+
+static void tcp_conn_handler(const struct sa *peer, void *arg)
+{
+ struct ctrl_st *st = arg;
+
+ (void)peer;
+
+ /* only one connection allowed */
+ st->tc = mem_deref(st->tc);
+ st->tc = mem_deref(st->ns);
+
+ (void)tcp_accept(&st->tc, st->ts, NULL, NULL, tcp_close_handler, st);
+ (void)netstring_insert(&st->ns, st->tc, 0, command_handler, st);
+}
+
+
+/*
+ * Relay UA events
+ */
+static void ua_event_handler(struct ua *ua, enum ua_event ev,
+ struct call *call, const char *prm, void *arg)
+{
+ struct ctrl_st *st = arg;
+ struct mbuf *buf = mbuf_alloc(1024);
+ struct re_printf pf = {print_handler, buf};
+ struct odict *od = NULL;
+ int err;
+
+ buf->pos = NETSTRING_HEADER_SIZE;
+
+ err = odict_alloc(&od, 8);
+ if (err)
+ return;
+
+ err = odict_entry_add(od, "event", ODICT_BOOL, true);
+ err |= event_encode_dict(od, ua, ev, call, prm);
+ if (err)
+ goto out;
+
+ err = json_encode_odict(&pf, od);
+ if (err) {
+ warning("ctrl_tcp: failed to encode json (%m)\n", err);
+ goto out;
+ }
+
+ if (st->tc) {
+ buf->pos = NETSTRING_HEADER_SIZE;
+ err = tcp_send(st->tc, buf);
+ if (err) {
+ warning("ctrl_tcp: failed to send the message (%m)\n",
+ err);
+ }
+ }
+
+ out:
+ mem_deref(buf);
+ mem_deref(od);
+}
+
+
+static void ctrl_destructor(void *arg)
+{
+ struct ctrl_st *st = arg;
+
+ mem_deref(st->tc);
+ mem_deref(st->ts);
+ mem_deref(st->ns);
+}
+
+
+static int ctrl_alloc(struct ctrl_st **stp, const struct sa *laddr)
+{
+ struct ctrl_st *st;
+ int err;
+
+ if (!stp)
+ return EINVAL;
+
+ st = mem_zalloc(sizeof(*st), ctrl_destructor);
+ if (!st)
+ return ENOMEM;
+
+ err = tcp_listen(&st->ts, laddr, tcp_conn_handler, st);
+ if (err) {
+ warning("ctrl_tcp: failed to listen on TCP %J (%m)\n",
+ laddr, err);
+ goto out;
+ }
+
+ debug("ctrl_tcp: TCP socket listening on %J\n", laddr);
+
+ out:
+ if (err)
+ mem_deref(st);
+ else
+ *stp = st;
+
+ return err;
+}
+
+
+static int ctrl_init(void)
+{
+ struct sa laddr;
+ int err;
+
+ if (conf_get_sa(conf_cur(), "ctrl_tcp_listen", &laddr)) {
+ sa_set_str(&laddr, "0.0.0.0", CTRL_PORT);
+ }
+
+ err = ctrl_alloc(&ctrl, &laddr);
+ if (err)
+ return err;
+
+ err = uag_event_register(ua_event_handler, ctrl);
+ if (err)
+ return err;
+
+ return 0;
+}
+
+
+static int ctrl_close(void)
+{
+ uag_event_unregister(ua_event_handler);
+ ctrl = mem_deref(ctrl);
+
+ return 0;
+}
+
+
+const struct mod_export DECL_EXPORTS(ctrl) = {
+ "ctrl_tcp",
+ "application",
+ ctrl_init,
+ ctrl_close
+};
diff --git a/modules/ctrl_tcp/module.mk b/modules/ctrl_tcp/module.mk
new file mode 100644
index 0000000..cf6f0f3
--- /dev/null
+++ b/modules/ctrl_tcp/module.mk
@@ -0,0 +1,10 @@
+#
+# module.mk
+#
+# Copyright (C) 2018 46 Labs LLC
+#
+
+MOD := ctrl_tcp
+$(MOD)_SRCS += ctrl_tcp.c tcp_netstring.c ./netstring/netstring.c
+
+include mk/mod.mk
diff --git a/modules/ctrl_tcp/netstring/netstring.c b/modules/ctrl_tcp/netstring/netstring.c
new file mode 100644
index 0000000..d429791
--- /dev/null
+++ b/modules/ctrl_tcp/netstring/netstring.c
@@ -0,0 +1,163 @@
+/* Streaming API for netstrings. */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <math.h>
+#include "netstring.h"
+
+
+const char* netstring_error_str(netstring_error err)
+{
+ switch (err) {
+ case NETSTRING_ERROR_TOO_LONG:
+ return "NETSTRING_ERROR_TOO_LONG";
+ case NETSTRING_ERROR_NO_COLON:
+ return "NETSTRING_ERROR_NO_COLON";
+ case NETSTRING_ERROR_TOO_SHORT:
+ return "NETSTRING_ERROR_TOO_SHORT";
+ case NETSTRING_ERROR_NO_COMMA:
+ return "NETSTRING_ERROR_NO_COMMA";
+ case NETSTRING_ERROR_LEADING_ZERO:
+ return "NETSTRING_ERROR_LEADING_ZERO";
+ case NETSTRING_ERROR_NO_LENGTH:
+ return "NETSTRING_ERROR_NO_LENGTH";
+ default:
+ return "NETSTRING_ERROR_UNKNOWN";
+ }
+}
+
+
+/**
+ * Reads a netstring from a `buffer` of length `buffer_length`. Writes
+ * to `netstring_start` a pointer to the beginning of the string in
+ * the buffer, and to `netstring_length` the length of the
+ * string. Does not allocate any memory. If it reads successfully,
+ * then it returns 0. If there is an error, then the return value will
+ * be negative. The error values are:
+
+ * NETSTRING_ERROR_TOO_LONG More than 999999999 bytes in a field
+ * NETSTRING_ERROR_NO_COLON No colon was found after the number
+ * NETSTRING_ERROR_TOO_SHORT Number of bytes greater than buffer length
+ * NETSTRING_ERROR_NO_COMMA No comma was found at the end
+ * NETSTRING_ERROR_LEADING_ZERO Leading zeros are not allowed
+ * NETSTRING_ERROR_NO_LENGTH Length not given at start of netstring
+
+ * If you're sending messages with more than 999999999 bytes -- about
+ * 2 GB -- then you probably should not be doing so in the form of a
+ * single netstring. This restriction is in place partially to protect
+ * from malicious or erroneous input, and partly to be compatible with
+ * D. J. Bernstein's reference implementation.
+
+ * Example:
+ * if (netstring_read("3:foo,", 6, &str, &len) < 0) explode_and_die();
+ */
+int netstring_read(char *buffer, size_t buffer_length,
+ char **netstring_start, size_t *netstring_length)
+{
+ size_t i;
+ size_t len = 0;
+
+ /* Write default values for outputs */
+ *netstring_start = NULL; *netstring_length = 0;
+
+ /* Make sure buffer is big enough. Minimum size is 3. */
+ if (buffer_length < 3)
+ return NETSTRING_ERROR_TOO_SHORT;
+
+ /* No leading zeros allowed! */
+ if (buffer[0] == '0' && isdigit(buffer[1]))
+ return NETSTRING_ERROR_LEADING_ZERO;
+
+ /* The netstring must start with a number */
+ if (!isdigit(buffer[0]))
+ return NETSTRING_ERROR_NO_LENGTH;
+
+ /* Read the number of bytes */
+ for (i = 0; i < buffer_length && isdigit(buffer[i]); i++) {
+
+ /* Error if more than 9 digits */
+ if (i >= 9)
+ return NETSTRING_ERROR_TOO_LONG;
+
+ /* Accumulate each digit, assuming ASCII. */
+ len = len*10 + (buffer[i] - '0');
+ }
+
+ /**
+ * Check buffer length. The buffer must be longer than the sum of:
+ * - the number we've read.
+ * - the length of the string itself.
+ * - the colon.
+ * - the comma.
+ */
+ if (i + len + 1 >= buffer_length)
+ return NETSTRING_ERROR_TOO_SHORT;
+
+ /* Read the colon */
+ if (buffer[i++] != ':')
+ return NETSTRING_ERROR_NO_COLON;
+
+ /* Test for the trailing comma, and set the return values */
+ if (buffer[i + len] != ',')
+ return NETSTRING_ERROR_NO_COMMA;
+
+ *netstring_start = &buffer[i]; *netstring_length = len;
+
+ return 0;
+}
+
+/**
+ * Return the number of digits represented in the given number.
+ * We are assuming that the input is not bigger than NETSTRING_MAX_SIZE.
+ */
+size_t netstring_num_len(size_t num)
+{
+ char num_str[10];
+
+ sprintf(num_str, "%zu", num);
+
+ return strlen(num_str);
+}
+
+/**
+ * Return the length, in ASCII characters, of a netstring containing
+ * `data_length` bytes.
+ */
+size_t netstring_buffer_size(size_t data_length)
+{
+ if (data_length == 0)
+ return 3;
+
+ return netstring_num_len(data_length) + data_length + 2;
+}
+
+/*
+ * Allocate and create a netstring containing the first `len` bytes of `data`.
+ * This must be manually freed by the client.
+ * If `len` is 0 then no data will be read from `data`, and it may be NULL.
+ */
+size_t netstring_encode_new(char **netstring, char *data, size_t len)
+{
+ char *ns;
+ size_t num_len = 1;
+
+ if (len == 0) {
+ ns = malloc(3);
+ ns[0] = '0';
+ ns[1] = ':';
+ ns[2] = ',';
+ }
+ else {
+ num_len = netstring_num_len(len);
+ ns = malloc(num_len + len + 2);
+ sprintf(ns, "%lu:", (unsigned long)len);
+ memcpy(ns + num_len + 1, data, len);
+ ns[num_len + len + 1] = ',';
+ }
+
+ *netstring = ns;
+
+ return num_len + len + 2;
+}
diff --git a/modules/ctrl_tcp/netstring/netstring.h b/modules/ctrl_tcp/netstring/netstring.h
new file mode 100644
index 0000000..a084428
--- /dev/null
+++ b/modules/ctrl_tcp/netstring/netstring.h
@@ -0,0 +1,28 @@
+#ifndef __NETSTRING_STREAM_H
+#define __NETSTRING_STREAM_H
+
+#include <string.h>
+
+const char* netstring_error_str(int err);
+
+int netstring_read(char *buffer, size_t buffer_length,
+ char **netstring_start, size_t *netstring_length);
+
+size_t netstring_num_len(size_t num);
+size_t netstring_buffer_size(size_t data_length);
+
+size_t netstring_encode_new(char **netstring, char *data, size_t len);
+
+#define NETSTRING_MAX_SIZE 999999999
+
+/* Errors that can occur during netstring parsing */
+typedef enum {
+ NETSTRING_ERROR_TOO_LONG = -100,
+ NETSTRING_ERROR_NO_COLON,
+ NETSTRING_ERROR_TOO_SHORT,
+ NETSTRING_ERROR_NO_COMMA,
+ NETSTRING_ERROR_LEADING_ZERO,
+ NETSTRING_ERROR_NO_LENGTH
+} netstring_error;
+
+#endif
diff --git a/modules/ctrl_tcp/tcp_netstring.c b/modules/ctrl_tcp/tcp_netstring.c
new file mode 100644
index 0000000..7b025fd
--- /dev/null
+++ b/modules/ctrl_tcp/tcp_netstring.c
@@ -0,0 +1,221 @@
+/**
+ * @file tcp_netstring.c TCP netstring framing
+ *
+ * Copyright (C) 2018 46 Labs LLC
+ */
+
+#include <math.h>
+#include <string.h>
+
+#include <re_types.h>
+#include <re_fmt.h>
+#include <re_mem.h>
+#include <re_mbuf.h>
+#include <re_tcp.h>
+#include <re_net.h>
+
+#include "tcp_netstring.h"
+#include "netstring/netstring.h"
+
+
+#define DEBUG_MODULE "tcp_netstring"
+#define DEBUG_LEVEL 5
+#include <re_dbg.h>
+
+
+struct netstring {
+ struct tcp_conn *tc;
+ struct tcp_helper *th;
+ struct mbuf *mb;
+ netstring_frame_h *frameh;
+ void *arg;
+
+ uint64_t n_tx;
+ uint64_t n_rx;
+};
+
+
+/* responsible for adding the netstring header
+ - assumes that the sent MBUF contains a complete packet
+ */
+static bool netstring_send_handler(int *err, struct mbuf *mb, void *arg)
+{
+ struct netstring *netstring = arg;
+ size_t num_len;
+ char num_str[10];
+
+ if (mb->pos < NETSTRING_HEADER_SIZE) {
+ DEBUG_WARNING("send: not enough space for netstring header\n");
+ *err = ENOMEM;
+ return true;
+ }
+
+ if (mbuf_get_left(mb) > NETSTRING_MAX_SIZE) {
+ DEBUG_WARNING("send: buffer exceeds max size\n");
+ *err = EMSGSIZE;
+ return true;
+ }
+
+ /* Build the netstring. */
+ if (mbuf_get_left(mb) == 0) {
+ mb->buf[0] = '0';
+ mb->buf[1] = ':';
+ mb->buf[2] = ',';
+
+ mb->end += 3;
+
+ return false;
+ }
+
+ sprintf(num_str, "%zu", mbuf_get_left(mb));
+ num_len = strlen(num_str);
+
+ mb->pos = NETSTRING_HEADER_SIZE - (num_len + 1);
+ mbuf_write_mem(mb, (uint8_t*) num_str, num_len);
+ mb->pos = NETSTRING_HEADER_SIZE - (num_len + 1);
+ mb->buf[mb->pos + num_len] = ':';
+ mb->buf[mb->end] = ',';
+
+ mb->end += 1;
+
+ ++netstring->n_tx;
+
+ return false;
+}
+
+
+static bool netstring_recv_handler(int *errp, struct mbuf *mbx, bool *estab,
+ void *arg)
+{
+ struct netstring *netstring = arg;
+ int err = 0;
+ size_t pos = 0;
+ (void)estab;
+
+ /* handle re-assembly */
+ if (!netstring->mb) {
+ netstring->mb = mbuf_alloc(1024);
+ if (!netstring->mb) {
+ *errp = ENOMEM;
+ return true;
+ }
+ }
+
+ pos = netstring->mb->pos;
+
+ netstring->mb->pos = netstring->mb->end;
+
+ err = mbuf_write_mem(netstring->mb, mbuf_buf(mbx),
+ mbuf_get_left(mbx));
+ if (err)
+ goto out;
+
+ netstring->mb->pos = pos;
+
+ /* extract all NETSTRING-frames in the TCP-stream */
+ for (;;) {
+
+ size_t len, end;
+ struct mbuf mb;
+
+ if (mbuf_get_left(netstring->mb) < (3))
+ break;
+
+ err = netstring_read((char*)netstring->mb->buf,
+ netstring->mb->end,
+ (char**)&mb.buf, &len);
+ if (err) {
+
+ if (err == NETSTRING_ERROR_TOO_SHORT) {
+ DEBUG_INFO("receive: %s\n",
+ netstring_error_str(err));
+ }
+
+ else {
+ DEBUG_WARNING("receive: %s\n",
+ netstring_error_str(err));
+ netstring->mb = mem_deref(netstring->mb);
+ }
+
+ return false;
+ }
+
+ pos = netstring->mb->pos;
+ end = netstring->mb->end;
+
+ netstring->mb->end = pos + len;
+
+ ++netstring->n_rx;
+
+ netstring->frameh(&mb, netstring->arg);
+
+ netstring->mb->pos = pos + netstring_buffer_size(len);
+ netstring->mb->end = end;
+
+ if (netstring->mb->pos >= netstring->mb->end) {
+ netstring->mb = mem_deref(netstring->mb);
+ break;
+ }
+
+ continue;
+ }
+
+ out:
+ if (err)
+ *errp = err;
+
+ return true; /* always handled */
+}
+
+
+static void destructor(void *arg)
+{
+ struct netstring *netstring = arg;
+
+ mem_deref(netstring->th);
+ mem_deref(netstring->tc);
+ mem_deref(netstring->mb);
+}
+
+
+int netstring_insert(struct netstring **netstringp, struct tcp_conn *tc,
+ int layer, netstring_frame_h *frameh, void *arg)
+{
+ struct netstring *netstring;
+ int err;
+
+ if (!netstringp || !tc || !frameh)
+ return EINVAL;
+
+ netstring = mem_zalloc(sizeof(*netstring), destructor);
+ if (!netstring)
+ return ENOMEM;
+
+ netstring->tc = mem_ref(tc);
+ err = tcp_register_helper(&netstring->th, tc, layer, NULL,
+ netstring_send_handler,
+ netstring_recv_handler, netstring);
+ if (err)
+ goto out;
+
+ netstring->frameh = frameh;
+ netstring->arg = arg;
+
+ out:
+ if (err)
+ mem_deref(netstring);
+ else
+ *netstringp = netstring;
+
+ return err;
+}
+
+
+int netstring_debug(struct re_printf *pf, const struct netstring *netstring)
+{
+ if (!netstring)
+ return 0;
+
+ return re_hprintf(pf, "tx=%llu, rx=%llu",
+ netstring->n_tx, netstring->n_rx);
+}
diff --git a/modules/ctrl_tcp/tcp_netstring.h b/modules/ctrl_tcp/tcp_netstring.h
new file mode 100644
index 0000000..8195217
--- /dev/null
+++ b/modules/ctrl_tcp/tcp_netstring.h
@@ -0,0 +1,16 @@
+/**
+ * @file tcp_netstring.h TCP netstring framing
+ *
+ * Copyright (C) 2018 46 Labs LLC
+ */
+
+enum {NETSTRING_HEADER_SIZE = 10};
+
+struct netstring;
+
+typedef bool (netstring_frame_h)(struct mbuf *mb, void *arg);
+
+
+int netstring_insert(struct netstring **netstringp, struct tcp_conn *tc,
+ int layer, netstring_frame_h *frameh, void *arg);
+int netstring_debug(struct re_printf *pf, const struct netstring *netstring);
diff --git a/src/ua.c b/src/ua.c
index 0467e35..e23d392 100644
--- a/src/ua.c
+++ b/src/ua.c
@@ -837,7 +837,8 @@ void ua_hangup(struct ua *ua, struct call *call,
(void)call_hangup(call, scode, reason);
- ua_event(ua, UA_EVENT_CALL_CLOSED, call, reason);
+ ua_event(ua, UA_EVENT_CALL_CLOSED, call,
+ reason ? reason : "Connection reset by user");
mem_deref(call);