diff options
-rw-r--r-- | mk/modules.mk | 1 | ||||
-rw-r--r-- | modules/ctrl_tcp/ctrl_tcp.c | 377 | ||||
-rw-r--r-- | modules/ctrl_tcp/module.mk | 10 | ||||
-rw-r--r-- | modules/ctrl_tcp/netstring/netstring.c | 163 | ||||
-rw-r--r-- | modules/ctrl_tcp/netstring/netstring.h | 28 | ||||
-rw-r--r-- | modules/ctrl_tcp/tcp_netstring.c | 221 | ||||
-rw-r--r-- | modules/ctrl_tcp/tcp_netstring.h | 16 | ||||
-rw-r--r-- | src/ua.c | 3 |
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); @@ -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); |