summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Michelson <mmichelson@digium.com>2012-10-12 16:31:01 +0000
committerMark Michelson <mmichelson@digium.com>2012-10-12 16:31:01 +0000
commitc7b23cbb0af1928014cb283c511b9dec48488163 (patch)
treeb9d0286502a98f9fdffbbd94c4150f616ddef54b
parent399428224d0a4d22b401fc228056ced705c07bdd (diff)
Do not use a FILE handle when doing SIP TCP reads.
This is used to solve an issue where a poll on a file descriptor does not necessarily correspond to the readiness of a FILE handle to be read. This change makes it so that for TCP connections, we do a recv() on the file descriptor instead. Because TCP does not guarantee that an entire message or even just one single message will arrive during a read, a loop has been introduced to ensure that we only attempt to handle a single message at a time. The tcptls_session_instance structure has also had an overflow buffer added to it so that if more than one TCP message arrives in one go, there is a place to throw the excess. Huge thanks goes out to Walter Doekes for doing extensive review on this change and finding edge cases where code could fail. (closes issue ASTERISK-20212) reported by Phil Ciccone Review: https://reviewboard.asterisk.org/r/2123 ........ Merged revisions 374905 from http://svn.asterisk.org/svn/asterisk/branches/1.8 ........ Merged revisions 374906 from http://svn.asterisk.org/svn/asterisk/branches/10 ........ Merged revisions 374914 from http://svn.asterisk.org/svn/asterisk/branches/11 git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@374924 65c4cc65-6c06-0410-ace0-fbb531ad65f3
-rw-r--r--channels/chan_sip.c969
-rw-r--r--include/asterisk/tcptls.h6
-rw-r--r--main/tcptls.c12
3 files changed, 868 insertions, 119 deletions
diff --git a/channels/chan_sip.c b/channels/chan_sip.c
index 4b86ab384..ae5d2c128 100644
--- a/channels/chan_sip.c
+++ b/channels/chan_sip.c
@@ -2686,12 +2686,363 @@ static int sip_check_authtimeout(time_t start)
return timeout;
}
+/*!
+ * \brief Read a SIP request or response from a TLS connection
+ *
+ * Because TLS operations are hidden from view via a FILE handle, the
+ * logic for reading data is a bit complex, and we have to make periodic
+ * checks to be sure we aren't taking too long to perform the necessary
+ * action.
+ *
+ * \todo XXX This should be altered in the future not to use a FILE pointer
+ *
+ * \param req The request structure to fill in
+ * \param tcptls_session The TLS connection on which the data is being received
+ * \param authenticated A flag indicating whether authentication has occurred yet.
+ * This is only relevant in a server role.
+ * \param start The time at which we started attempting to read data. Used in
+ * determining if there has been a timeout.
+ * \param me Thread info. Used as a means of determining if the session needs to be stoppped.
+ * \retval -1 Failed to read data
+ * \retval 0 Succeeded in reading data
+ */
+static int sip_tls_read(struct sip_request *req, struct ast_tcptls_session_instance *tcptls_session, int authenticated, time_t start, struct sip_threadinfo *me)
+{
+ int res, content_length, after_poll = 1, need_poll = 1;
+ struct sip_request reqcpy = { 0, };
+ char buf[1024] = "";
+ int timeout = -1;
+
+ /* Read in headers one line at a time */
+ while (ast_str_strlen(req->data) < 4 || strncmp(REQ_OFFSET_TO_STR(req, data->used - 4), "\r\n\r\n", 4)) {
+ if (!tcptls_session->client && !authenticated) {
+ if ((timeout = sip_check_authtimeout(start)) < 0) {
+ ast_debug(2, "SIP SSL server failed to determine authentication timeout\n");
+ return -1;
+ }
+
+ if (timeout == 0) {
+ ast_debug(2, "SIP %s server timed out\n", tcptls_session->ssl ? "SSL": "TCP");
+ return -1;
+ }
+ } else {
+ timeout = -1;
+ }
+
+ /* special polling behavior is required for TLS
+ * sockets because of the buffering done in the
+ * TLS layer */
+ if (need_poll) {
+ need_poll = 0;
+ after_poll = 1;
+ res = ast_wait_for_input(tcptls_session->fd, timeout);
+ if (res < 0) {
+ ast_debug(2, "SIP TCP server :: ast_wait_for_input returned %d\n", res);
+ return -1;
+ } else if (res == 0) {
+ /* timeout */
+ ast_debug(2, "SIP TCP server timed out\n");
+ return -1;
+ }
+ }
+
+ ao2_lock(tcptls_session);
+ if (!fgets(buf, sizeof(buf), tcptls_session->f)) {
+ ao2_unlock(tcptls_session);
+ if (after_poll) {
+ return -1;
+ } else {
+ need_poll = 1;
+ continue;
+ }
+ }
+ ao2_unlock(tcptls_session);
+ after_poll = 0;
+ if (me->stop) {
+ return -1;
+ }
+ ast_str_append(&req->data, 0, "%s", buf);
+ }
+ copy_request(&reqcpy, req);
+ parse_request(&reqcpy);
+ /* In order to know how much to read, we need the content-length header */
+ if (sscanf(sip_get_header(&reqcpy, "Content-Length"), "%30d", &content_length)) {
+ while (content_length > 0) {
+ size_t bytes_read;
+ if (!tcptls_session->client && !authenticated) {
+ if ((timeout = sip_check_authtimeout(start)) < 0) {
+ return -1;
+ }
+
+ if (timeout == 0) {
+ ast_debug(2, "SIP SSL server timed out\n");
+ return -1;
+ }
+ } else {
+ timeout = -1;
+ }
+
+ if (need_poll) {
+ need_poll = 0;
+ after_poll = 1;
+ res = ast_wait_for_input(tcptls_session->fd, timeout);
+ if (res < 0) {
+ ast_debug(2, "SIP TCP server :: ast_wait_for_input returned %d\n", res);
+ return -1;
+ } else if (res == 0) {
+ /* timeout */
+ ast_debug(2, "SIP TCP server timed out\n");
+ return -1;
+ }
+ }
+
+ ao2_lock(tcptls_session);
+ if (!(bytes_read = fread(buf, 1, MIN(sizeof(buf) - 1, content_length), tcptls_session->f))) {
+ ao2_unlock(tcptls_session);
+ if (after_poll) {
+ return -1;
+ } else {
+ need_poll = 1;
+ continue;
+ }
+ }
+ buf[bytes_read] = '\0';
+ ao2_unlock(tcptls_session);
+ after_poll = 0;
+ if (me->stop) {
+ return -1;
+ }
+ content_length -= strlen(buf);
+ ast_str_append(&req->data, 0, "%s", buf);
+ }
+ }
+ /*! \todo XXX If there's no Content-Length or if the content-length and what
+ we receive is not the same - we should generate an error */
+ return 0;
+}
+
+/*!
+ * \brief Indication of a TCP message's integrity
+ */
+enum message_integrity {
+ /*!
+ * The message has an error in it with
+ * regards to its Content-Length header
+ */
+ MESSAGE_INVALID,
+ /*!
+ * The message is incomplete
+ */
+ MESSAGE_FRAGMENT,
+ /*!
+ * The data contains a complete message
+ * plus a fragment of another.
+ */
+ MESSAGE_FRAGMENT_COMPLETE,
+ /*!
+ * The message is complete
+ */
+ MESSAGE_COMPLETE,
+};
+
+/*!
+ * \brief
+ * Get the content length from an unparsed SIP message
+ *
+ * \param message The unparsed SIP message headers
+ * \return The value of the Content-Length header or -1 if message is invalid
+ */
+static int read_raw_content_length(const char *message)
+{
+ char *end_of_line;
+ char *content_length_str;
+ char *l_str;
+ int content_length;
+ char *msg;
+
+ if (sip_cfg.pedanticsipchecking) {
+ struct ast_str *msg_copy = ast_str_create(strlen(message));
+ if (!msg_copy) {
+ return -1;
+ }
+ ast_str_set(&msg_copy, 0, "%s", message);
+ lws2sws(msg_copy);
+ msg = ast_strdupa(ast_str_buffer(msg_copy));
+ ast_free(msg_copy);
+ } else {
+ msg = ast_strdupa(message);
+ }
+
+ /* Let's find a Content-Length header */
+ content_length_str = strcasestr(msg, "\nContent-Length:");
+ if (!content_length_str && !(l_str = strcasestr(msg, "\nl:"))) {
+ /* RFC 3261 18.3
+ * "In the case of stream-oriented transports such as TCP, the Content-
+ * Length header field indicates the size of the body. The Content-
+ * Length header field MUST be used with stream oriented transports."
+ */
+ return -1;
+ }
+ if (content_length_str) {
+ content_length_str += sizeof("\nContent-Length:");
+ } else if (l_str) {
+ content_length_str = l_str + sizeof("\nl:");
+ } else {
+ return -1;
+ }
+
+ end_of_line = strchr(content_length_str, '\n');
+
+ if (!end_of_line) {
+ return -1;
+ }
+
+ if (sscanf(content_length_str, "%30d", &content_length) == 1) {
+ return content_length;
+ }
+
+ return -1;
+}
+
+/*!
+ * \brief Check that a message received over TCP is a full message
+ *
+ * This will take the information read in and then determine if
+ * 1) The message is a full SIP request
+ * 2) The message is a partial SIP request
+ * 3) The message contains a full SIP request along with another partial request
+ * \param data The unparsed incoming SIP message.
+ * \param request The resulting request with extra fragments removed.
+ * \param overflow If the message contains more than a full request, this is the remainder of the message
+ * \return The resulting integrity of the message
+ */
+static enum message_integrity check_message_integrity(struct ast_str **request, struct ast_str **overflow)
+{
+ char *message = ast_strdupa(ast_str_buffer(*request));
+ char *body;
+ int content_length;
+ int body_len;
+ int message_len = strlen(message);
+
+ /* Important pieces to search for in a SIP request are \r\n\r\n. This
+ * marks either
+ * 1) The division between the headers and body
+ * 2) The end of the SIP request
+ */
+ body = strstr(message, "\r\n\r\n");
+ if (!body) {
+ /* This is clearly a partial message since we haven't reached an end
+ * yet.
+ */
+ return MESSAGE_FRAGMENT;
+ }
+ body += sizeof("\r\n\r\n") - 1;
+ body_len = strlen(body);
+
+ body[-1] = '\0';
+ content_length = read_raw_content_length(message);
+ body[-1] = '\n';
+
+ if (content_length < 0) {
+ return MESSAGE_INVALID;
+ } else if (content_length == 0) {
+ /* We've definitely received an entire message. We need
+ * to check if there's also a fragment of another message
+ * in addition.
+ */
+ if (body_len == 0) {
+ return MESSAGE_COMPLETE;
+ } else {
+ ast_str_truncate(*request, message_len - body_len);
+ ast_str_append(overflow, 0, "%s", body);
+ return MESSAGE_FRAGMENT_COMPLETE;
+ }
+ }
+ /* Positive content length. Let's see what sort of
+ * message body we're dealing with.
+ */
+ if (body_len < content_length) {
+ /* We don't have the full message body yet */
+ return MESSAGE_FRAGMENT;
+ } else if (body_len > content_length) {
+ /* We have the full message plus a fragment of a further
+ * message
+ */
+ ast_str_truncate(*request, message_len - (body_len - content_length));
+ ast_str_append(overflow, 0, "%s", body + content_length);
+ return MESSAGE_FRAGMENT_COMPLETE;
+ } else {
+ /* Yay! Full message with no extra content */
+ return MESSAGE_COMPLETE;
+ }
+}
+
+/*!
+ * \brief Read SIP request or response from a TCP connection
+ *
+ * \param req The request structure to be filled in
+ * \param tcptls_session The TCP connection from which to read
+ * \retval -1 Failed to read data
+ * \retval 0 Successfully read data
+ */
+static int sip_tcp_read(struct sip_request *req, struct ast_tcptls_session_instance *tcptls_session,
+ int authenticated, time_t start)
+{
+ enum message_integrity message_integrity = MESSAGE_FRAGMENT;
+
+ while (message_integrity == MESSAGE_FRAGMENT) {
+ if (ast_str_strlen(tcptls_session->overflow_buf) == 0) {
+ char readbuf[4097];
+ int timeout;
+ int res;
+ if (!tcptls_session->client && !authenticated) {
+ if ((timeout = sip_check_authtimeout(start)) < 0) {
+ return -1;
+ }
+
+ if (timeout == 0) {
+ ast_debug(2, "SIP TCP server timed out\n");
+ return -1;
+ }
+ } else {
+ timeout = -1;
+ }
+ res = ast_wait_for_input(tcptls_session->fd, timeout);
+ if (res < 0) {
+ ast_debug(2, "SIP TCP server :: ast_wait_for_input returned %d\n", res);
+ return -1;
+ } else if (res == 0) {
+ ast_debug(2, "SIP TCP server timed out\n");
+ return -1;
+ }
+
+ res = recv(tcptls_session->fd, readbuf, sizeof(readbuf) - 1, 0);
+ if (res < 0) {
+ ast_debug(2, "SIP TCP server error when receiving data\n");
+ return -1;
+ } else if (res == 0) {
+ ast_debug(2, "SIP TCP server has shut down\n");
+ return -1;
+ }
+ readbuf[res] = '\0';
+ ast_str_append(&req->data, 0, "%s", readbuf);
+ } else {
+ ast_str_append(&req->data, 0, "%s", ast_str_buffer(tcptls_session->overflow_buf));
+ ast_str_reset(tcptls_session->overflow_buf);
+ }
+
+ message_integrity = check_message_integrity(&req->data, &tcptls_session->overflow_buf);
+ }
+
+ return 0;
+}
+
/*! \brief SIP TCP thread management function
This function reads from the socket, parses the packet into a request
*/
static void *_sip_tcp_helper_thread(struct ast_tcptls_session_instance *tcptls_session)
{
- int res, cl, timeout = -1, authenticated = 0, flags, after_poll = 0, need_poll = 1;
+ int res, timeout = -1, authenticated = 0, flags;
time_t start;
struct sip_request req = { 0, } , reqcpy = { 0, };
struct sip_threadinfo *me = NULL;
@@ -2791,21 +3142,23 @@ static void *_sip_tcp_helper_thread(struct ast_tcptls_session_instance *tcptls_s
timeout = -1;
}
- res = ast_poll(fds, 2, timeout); /* polls for both socket and alert_pipe */
- if (res < 0) {
- ast_debug(2, "SIP %s server :: ast_wait_for_input returned %d\n", tcptls_session->ssl ? "SSL": "TCP", res);
- goto cleanup;
- } else if (res == 0) {
- /* timeout */
- ast_debug(2, "SIP %s server timed out\n", tcptls_session->ssl ? "SSL": "TCP");
- goto cleanup;
+ if (ast_str_strlen(tcptls_session->overflow_buf) == 0) {
+ res = ast_poll(fds, 2, timeout); /* polls for both socket and alert_pipe */
+ if (res < 0) {
+ ast_debug(2, "SIP %s server :: ast_wait_for_input returned %d\n", tcptls_session->ssl ? "SSL": "TCP", res);
+ goto cleanup;
+ } else if (res == 0) {
+ /* timeout */
+ ast_debug(2, "SIP %s server timed out\n", tcptls_session->ssl ? "SSL": "TCP");
+ goto cleanup;
+ }
}
- /* handle the socket event, check for both reads from the socket fd,
- * and writes from alert_pipe fd */
- if (fds[0].revents) { /* there is data on the socket to be read */
- after_poll = 1;
-
+ /*
+ * handle the socket event, check for both reads from the socket fd or TCP overflow buffer,
+ * and writes from alert_pipe fd.
+ */
+ if (fds[0].revents || (ast_str_strlen(tcptls_session->overflow_buf) > 0)) { /* there is data on the socket to be read */
fds[0].revents = 0;
/* clear request structure */
@@ -2829,111 +3182,15 @@ static void *_sip_tcp_helper_thread(struct ast_tcptls_session_instance *tcptls_s
req.socket.port = htons(ourport_tcp);
}
req.socket.fd = tcptls_session->fd;
+ if (tcptls_session->ssl) {
+ res = sip_tls_read(&req, tcptls_session, authenticated, start, me);
+ } else {
+ res = sip_tcp_read(&req, tcptls_session, authenticated, start);
+ }
- /* Read in headers one line at a time */
- while (ast_str_strlen(req.data) < 4 || strncmp(REQ_OFFSET_TO_STR(&req, data->used - 4), "\r\n\r\n", 4)) {
- if (!tcptls_session->client && !authenticated ) {
- if ((timeout = sip_check_authtimeout(start)) < 0) {
- goto cleanup;
- }
-
- if (timeout == 0) {
- ast_debug(2, "SIP %s server timed out\n", tcptls_session->ssl ? "SSL": "TCP");
- goto cleanup;
- }
- } else {
- timeout = -1;
- }
-
- /* special polling behavior is required for TLS
- * sockets because of the buffering done in the
- * TLS layer */
- if (!tcptls_session->ssl || need_poll) {
- need_poll = 0;
- after_poll = 1;
- res = ast_wait_for_input(tcptls_session->fd, timeout);
- if (res < 0) {
- ast_debug(2, "SIP TCP server :: ast_wait_for_input returned %d\n", res);
- goto cleanup;
- } else if (res == 0) {
- /* timeout */
- ast_debug(2, "SIP TCP server timed out\n");
- goto cleanup;
- }
- }
-
- ao2_lock(tcptls_session);
- if (!fgets(buf, sizeof(buf), tcptls_session->f)) {
- ao2_unlock(tcptls_session);
- if (after_poll) {
- goto cleanup;
- } else {
- need_poll = 1;
- continue;
- }
- }
- ao2_unlock(tcptls_session);
- after_poll = 0;
- if (me->stop) {
- goto cleanup;
- }
- ast_str_append(&req.data, 0, "%s", buf);
- }
- copy_request(&reqcpy, &req);
- parse_request(&reqcpy);
- /* In order to know how much to read, we need the content-length header */
- if (sscanf(sip_get_header(&reqcpy, "Content-Length"), "%30d", &cl)) {
- while (cl > 0) {
- size_t bytes_read;
- if (!tcptls_session->client && !authenticated ) {
- if ((timeout = sip_check_authtimeout(start)) < 0) {
- goto cleanup;
- }
-
- if (timeout == 0) {
- ast_debug(2, "SIP %s server timed out\n", tcptls_session->ssl ? "SSL": "TCP");
- goto cleanup;
- }
- } else {
- timeout = -1;
- }
-
- if (!tcptls_session->ssl || need_poll) {
- need_poll = 0;
- after_poll = 1;
- res = ast_wait_for_input(tcptls_session->fd, timeout);
- if (res < 0) {
- ast_debug(2, "SIP TCP server :: ast_wait_for_input returned %d\n", res);
- goto cleanup;
- } else if (res == 0) {
- /* timeout */
- ast_debug(2, "SIP TCP server timed out\n");
- goto cleanup;
- }
- }
-
- ao2_lock(tcptls_session);
- if (!(bytes_read = fread(buf, 1, MIN(sizeof(buf) - 1, cl), tcptls_session->f))) {
- ao2_unlock(tcptls_session);
- if (after_poll) {
- goto cleanup;
- } else {
- need_poll = 1;
- continue;
- }
- }
- buf[bytes_read] = '\0';
- ao2_unlock(tcptls_session);
- after_poll = 0;
- if (me->stop) {
- goto cleanup;
- }
- cl -= strlen(buf);
- ast_str_append(&req.data, 0, "%s", buf);
- }
+ if (res < 0) {
+ goto cleanup;
}
- /*! \todo XXX If there's no Content-Length or if the content-length and what
- we receive is not the same - we should generate an error */
req.socket.tcptls_session = tcptls_session;
req.socket.ws_session = NULL;
@@ -33132,6 +33389,482 @@ AST_TEST_DEFINE(test_sip_peers_get)
return AST_TEST_PASS;
}
+/*!
+ * \brief Imitation TCP reception loop
+ *
+ * This imitates the logic used by SIP's TCP code. Its purpose
+ * is to either
+ * 1) Combine fragments into a single message
+ * 2) Break up combined messages into single messages
+ *
+ * \param fragments The message fragments. This simulates the data received on a TCP socket.
+ * \param num_fragments This indicates the number of fragments to receive
+ * \param overflow This is a place to stash extra data if more than one message is received
+ * in a single fragment
+ * \param[out] messages The parsed messages are placed in this array
+ * \param[out] num_messages The number of messages that were parsed
+ * \param test Used for printing messages
+ * \retval 0 Success
+ * \retval -1 Failure
+ */
+static int mock_tcp_loop(char *fragments[], size_t num_fragments,
+ struct ast_str **overflow, char **messages, int *num_messages, struct ast_test* test)
+{
+ struct ast_str *req_data;
+ int i = 0;
+ int res = 0;
+
+ req_data = ast_str_create(128);
+ ast_str_reset(*overflow);
+
+ while (i < num_fragments || ast_str_strlen(*overflow) > 0) {
+ enum message_integrity message_integrity = MESSAGE_FRAGMENT;
+ ast_str_reset(req_data);
+ while (message_integrity == MESSAGE_FRAGMENT) {
+ if (ast_str_strlen(*overflow) > 0) {
+ ast_str_append(&req_data, 0, "%s", ast_str_buffer(*overflow));
+ ast_str_reset(*overflow);
+ } else {
+ ast_str_append(&req_data, 0, "%s", fragments[i++]);
+ }
+ message_integrity = check_message_integrity(&req_data, overflow);
+ }
+ if (strcmp(ast_str_buffer(req_data), messages[*num_messages])) {
+ ast_test_status_update(test, "Mismatch in SIP messages.\n");
+ ast_test_status_update(test, "Expected message:\n%s", messages[*num_messages]);
+ ast_test_status_update(test, "Parsed message:\n%s", ast_str_buffer(req_data));
+ res = -1;
+ goto end;
+ } else {
+ ast_test_status_update(test, "Successfully read message:\n%s", ast_str_buffer(req_data));
+ }
+ (*num_messages)++;
+ }
+
+end:
+ ast_free(req_data);
+ return res;
+};
+
+AST_TEST_DEFINE(test_tcp_message_fragmentation)
+{
+ /* Normal single message in one fragment */
+ char *normal[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+
+ /* Single message in two fragments.
+ * Fragments combine to make "normal"
+ */
+ char *fragmented[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: ",
+
+ "70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+ /* Single message in two fragments, divided precisely at the body
+ * Fragments combine to make "normal"
+ */
+ char *fragmented_body[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n",
+
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+
+ /* Single message in three fragments
+ * Fragments combine to make "normal"
+ */
+ char *multi_fragment[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n",
+
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4",
+
+ " 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+
+ /* Two messages in a single fragment
+ * Fragments split into "multi_message_divided"
+ */
+ char *multi_message[] = {
+ "SIP/2.0 100 Trying\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: <sip:bob@example.org:5060>\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n"
+ "SIP/2.0 180 Ringing\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: <sip:bob@example.org:5060>\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n"
+ };
+ char *multi_message_divided[] = {
+ "SIP/2.0 100 Trying\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: <sip:bob@example.org:5060>\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n",
+
+ "SIP/2.0 180 Ringing\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: <sip:bob@example.org:5060>\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n"
+ };
+ /* Two messages with bodies combined into one fragment
+ * Fragments split into "multi_message_body_divided"
+ */
+ char *multi_message_body[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 2 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+ char *multi_message_body_divided[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n",
+
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 2 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+
+ /* Two messages that appear in two fragments. Fragment
+ * boundaries do not align with message boundaries.
+ * Fragments combine to make "multi_message_divided"
+ */
+ char *multi_message_in_fragments[] = {
+ "SIP/2.0 100 Trying\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVI",
+
+ "TE\r\n"
+ "Contact: <sip:bob@example.org:5060>\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n"
+ "SIP/2.0 180 Ringing\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: <sip:bob@example.org:5060>\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n"
+ };
+
+ /* Message with compact content-length header
+ * Same as "normal" but with compact content-length header
+ */
+ char *compact[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "l: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+
+ /* Message with faux content-length headers
+ * Same as "normal" but with extra fake content-length headers
+ */
+ char *faux[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "DisContent-Length: 0\r\n"
+ "MalContent-Length: 60\r\n"
+ "Content-Length: 130\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+
+ /* Message with folded Content-Length header
+ * Message is "normal" with Content-Length spread across three lines
+ *
+ * This is the test that requires pedantic=yes in order to pass
+ */
+ char *folded[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: \t\r\n"
+ "\t \r\n"
+ " 130\t \r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+
+ /* Message with compact Content-length header in message and
+ * full Content-Length header in the body. Ensure that the header
+ * in the message is read and that the one in the body is ignored
+ */
+ char *cl_in_body[] = {
+ "INVITE sip:bob@example.org SIP/2.0\r\n"
+ "Via: SIP/2.0/TCP 127.0.0.1:5060;branch=[branch]\r\n"
+ "From: sipp <sip:127.0.0.1:5061>;tag=12345\r\n"
+ "To: <sip:bob@example.org:5060>\r\n"
+ "Call-ID: 12345\r\n"
+ "CSeq: 1 INVITE\r\n"
+ "Contact: sip:127.0.0.1:5061\r\n"
+ "Max-Forwards: 70\r\n"
+ "Content-Type: application/sdp\r\n"
+ "l: 149\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "Content-Length: 0\r\n"
+ "o=user1 53655765 2353687637 IN IP4 127.0.0.1\r\n"
+ "s=-\r\n"
+ "c=IN IP4 127.0.0.1\r\n"
+ "t=0 0\r\n"
+ "m=audio 10000 RTP/AVP 0\r\n"
+ "a=rtpmap:0 PCMU/8000\r\n"
+ };
+
+ struct ast_str *overflow;
+ struct {
+ char **fragments;
+ char **expected;
+ int num_expected;
+ const char *description;
+ } tests[] = {
+ { normal, normal, 1, "normal" },
+ { fragmented, normal, 1, "fragmented" },
+ { fragmented_body, normal, 1, "fragmented_body" },
+ { multi_fragment, normal, 1, "multi_fragment" },
+ { multi_message, multi_message_divided, 2, "multi_message" },
+ { multi_message_body, multi_message_body_divided, 2, "multi_message_body" },
+ { multi_message_in_fragments, multi_message_divided, 2, "multi_message_in_fragments" },
+ { compact, compact, 1, "compact" },
+ { faux, faux, 1, "faux" },
+ { folded, folded, 1, "folded" },
+ { cl_in_body, cl_in_body, 1, "cl_in_body" },
+ };
+ int i;
+ enum ast_test_result_state res = AST_TEST_PASS;
+
+ switch (cmd) {
+ case TEST_INIT:
+ info->name = "sip_tcp_message_fragmentation";
+ info->category = "/main/sip/transport";
+ info->summary = "SIP TCP message fragmentation test";
+ info->description =
+ "Tests reception of different TCP messages that have been fragmented or"
+ "run together. This test mimicks the code that TCP reception uses.";
+ return AST_TEST_NOT_RUN;
+ case TEST_EXECUTE:
+ break;
+ }
+ if (!sip_cfg.pedanticsipchecking) {
+ ast_log(LOG_WARNING, "Not running test. Pedantic SIP checking is not enabled, so it is guaranteed to fail\n");
+ return AST_TEST_NOT_RUN;
+ }
+
+ overflow = ast_str_create(128);
+ if (!overflow) {
+ return AST_TEST_FAIL;
+ }
+ for (i = 0; i < ARRAY_LEN(tests); ++i) {
+ int num_messages = 0;
+ if (mock_tcp_loop(tests[i].fragments, ARRAY_LEN(tests[i].fragments),
+ &overflow, tests[i].expected, &num_messages, test)) {
+ ast_test_status_update(test, "Failed to parse message '%s'\n", tests[i].description);
+ res = AST_TEST_FAIL;
+ break;
+ }
+ if (num_messages != tests[i].num_expected) {
+ ast_test_status_update(test, "Did not receive the expected number of messages. "
+ "Expected %d but received %d\n", tests[i].num_expected, num_messages);
+ res = AST_TEST_FAIL;
+ break;
+ }
+ }
+ ast_free(overflow);
+ return res;
+}
+
#endif
#define DATA_EXPORT_SIP_PEER(MEMBER) \
@@ -33390,6 +34123,7 @@ static int load_module(void)
#ifdef TEST_FRAMEWORK
AST_TEST_REGISTER(test_sip_peers_get);
AST_TEST_REGISTER(test_sip_mwi_subscribe_parse);
+ AST_TEST_REGISTER(test_tcp_message_fragmentation);
#endif
/* Register AstData providers */
@@ -33517,6 +34251,7 @@ static int unload_module(void)
AST_TEST_UNREGISTER(test_sip_peers_get);
AST_TEST_UNREGISTER(test_sip_mwi_subscribe_parse);
+ AST_TEST_UNREGISTER(test_tcp_message_fragmentation);
#endif
/* Unregister all the AstData providers */
ast_data_unregister(NULL);
diff --git a/include/asterisk/tcptls.h b/include/asterisk/tcptls.h
index 6d8d14993..6364158de 100644
--- a/include/asterisk/tcptls.h
+++ b/include/asterisk/tcptls.h
@@ -155,6 +155,12 @@ struct ast_tcptls_session_instance {
int client;
struct ast_sockaddr remote_address;
struct ast_tcptls_session_args *parent;
+ /* Sometimes, when an entity reads TCP data, multiple
+ * logical messages might be read at the same time. In such
+ * a circumstance, there needs to be a place to stash the
+ * extra data.
+ */
+ struct ast_str *overflow_buf;
};
#if defined(HAVE_FUNOPEN)
diff --git a/main/tcptls.c b/main/tcptls.c
index 2e5cbf4c6..b52cdc491 100644
--- a/main/tcptls.c
+++ b/main/tcptls.c
@@ -142,6 +142,12 @@ HOOK_T ast_tcptls_server_write(struct ast_tcptls_session_instance *tcptls_sessio
return write(tcptls_session->fd, buf, count);
}
+static void session_instance_destructor(void *obj)
+{
+ struct ast_tcptls_session_instance *i = obj;
+ ast_free(i->overflow_buf);
+}
+
/*! \brief
* creates a FILE * from the fd passed by the accept thread.
* This operation is potentially expensive (certificate verification),
@@ -290,7 +296,7 @@ void *ast_tcptls_server_root(void *data)
}
continue;
}
- tcptls_session = ao2_alloc(sizeof(*tcptls_session), NULL);
+ tcptls_session = ao2_alloc(sizeof(*tcptls_session), session_instance_destructor);
if (!tcptls_session) {
ast_log(LOG_WARNING, "No memory for new session: %s\n", strerror(errno));
if (close(fd)) {
@@ -299,6 +305,7 @@ void *ast_tcptls_server_root(void *data)
continue;
}
+ tcptls_session->overflow_buf = ast_str_create(128);
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
tcptls_session->fd = fd;
@@ -498,10 +505,11 @@ struct ast_tcptls_session_instance *ast_tcptls_client_create(struct ast_tcptls_s
}
}
- if (!(tcptls_session = ao2_alloc(sizeof(*tcptls_session), NULL))) {
+ if (!(tcptls_session = ao2_alloc(sizeof(*tcptls_session), session_instance_destructor))) {
goto error;
}
+ tcptls_session->overflow_buf = ast_str_create(128);
tcptls_session->client = 1;
tcptls_session->fd = desc->accept_fd;
tcptls_session->parent = desc;